Skip to content

Fix: Ensure explicit undefined on optional members#2109

Open
huntie wants to merge 1 commit into
facebook:static_hfrom
huntie:export-D113033807
Open

Fix: Ensure explicit undefined on optional members#2109
huntie wants to merge 1 commit into
facebook:static_hfrom
huntie:export-D113033807

Conversation

@huntie

@huntie huntie commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary:
Motivation

See react/react-native#57628.

Flow optional object members permit undefined as a value, so the faithful TypeScript translation of {foo?: T} is {foo?: T | undefined}.

The bare {foo?: T} form is stricter under TypeScript compared to Flow:

// Flow's semantics

interface Props {
  onRefresh?: () => void;
}
const a: Props = { onRefresh: undefined }; // ✅ ok
// TypeScript with exactOptionalPropertyTypes: true (i.e. strict mode)

interface Props {
  onRefresh?: () => void;
}
const a: Props = { onRefresh: undefined }; // ❌ error

interface PropsFixed {
  onRefresh?: (() => void) | undefined;
}
const b: PropsFixed = { onRefresh: undefined }; // ✅ ok

This diff

This makes the translator emit the explicit | undefined on optional members by default. Optional members (?:) now translate as wider resulting types:

foo?: number => foo?: number | undefined
foo?: () => void => foo?: (() => void) | undefined

Members whose type already includes undefined (e.g. the maybe type foo?: ?Tnull | undefined | T) are left unchanged (no duplicate | undefined). Required members are untouched.

NOTE: This is a behavior change: 3 existing fixtures are updated. This is borderline for counting as semver breaking for this project — emitted types are now wider.

Differential Revision: D113033807

Summary:
**Motivation**

See react/react-native#57628.

Flow optional object members permit `undefined` as a value, so the faithful TypeScript translation of `{foo?: T}` is `{foo?: T | undefined}`.

The bare `{foo?: T}` form is stricter under TypeScript compared to Flow:

```js
// Flow's semantics

interface Props {
  onRefresh?: () => void;
}
const a: Props = { onRefresh: undefined }; // ✅ ok
```
```ts
// TypeScript with exactOptionalPropertyTypes: true (i.e. strict mode)

interface Props {
  onRefresh?: () => void;
}
const a: Props = { onRefresh: undefined }; // ❌ error

interface PropsFixed {
  onRefresh?: (() => void) | undefined;
}
const b: PropsFixed = { onRefresh: undefined }; // ✅ ok
```

**This diff**

This makes the translator emit the explicit `| undefined` on optional members by default. Optional members (`?:`) now translate as wider resulting types:

  foo?: number        =>  foo?: number | undefined
  foo?: () => void    =>  foo?: (() => void) | undefined

Members whose type already includes `undefined` (e.g. the maybe type `foo?: ?T` → `null | undefined | T`) are left unchanged (no duplicate `| undefined`). Required members are untouched.

NOTE: **This is a behavior change**: 3 existing fixtures are updated. This is on-the-line for counting as semver breaking for this project — emitted types are now wider.

Differential Revision: D113033807
@meta-cla meta-cla Bot added the CLA Signed Do not delete this pull request or issue due to inactivity. label Jul 21, 2026
@meta-codesync

meta-codesync Bot commented Jul 21, 2026

Copy link
Copy Markdown

@huntie has exported this pull request. If you are a Meta employee, you can view the originating Diff in D113033807.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed Do not delete this pull request or issue due to inactivity. meta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant