Skip to content

fix(types): accept an explicit undefined on every prop this package adds - #418

Open
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:rncss/types-optional-undefined
Open

fix(types): accept an explicit undefined on every prop this package adds#418
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:rncss/types-optional-undefined

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Problem

React Native declares its own optional props as | undefined — 698 of them, and none bare. The props this package augments them with are bare ?: string. Under exactOptionalPropertyTypes those two forms are not the same, and the difference falls on the most ordinary thing a consumer writes:

<View className={condition ? "p-4" : undefined} />
error TS2769: Type '{ className: string | undefined; }' is not assignable to type 'Readonly<ViewProps>'
  with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.

Eleven authoring patterns fail this way — the conditional above, a string | undefined variable, a spread bag, a wrapper forwarding its own optional className, and the same for cssInterop, contentContainerClassName, indicatorClassName, placeholderClassName. A consumer cannot fix any of it locally: declaration merging can add props but never re-declare the optionality of existing ones. The only escape is patching the package or writing {...(cond ? {className: x} : {})} at every call site.

Fix

Every added prop takes | undefined, in both places the contract is expressed.

types.d.ts is the obvious one. src/runtime.types.ts is the one that is easy to miss: StyledProps, StyledReactElement and StyledComponent synthesise the same props through mapped types ending ]?: string, so they re-narrow it. Without those three tokens the bug still reproduces on react-native-css/components and styled() — the imports the README documents.

There is one observable change beyond permissiveness, and it goes toward consistency rather than away: under the flag, Required<ViewProps>['className'] previously stripped undefined while Required<ViewProps>['style'] kept it. Now they agree.

Tests

src/__tests__/types/ is a two-file program — the fixture plus types.d.ts — compiled with exactOptionalPropertyTypes on, wired into typecheck as a second tsc invocation. src/__tests__/babel/tsconfig.json is the existing precedent for a nested tsconfig here, and the leading underscore keeps the fixture out of jest via the existing testPathIgnorePatterns.

It is mutation-proven: reverting types.d.ts turns the gate red with 15 errors while the root typecheck stays green. That gap is the reason the gate exists — with the flag off, the old and new declarations are the same type, so no fixture compiled by the current typecheck can tell them apart. I checked the alternative first: a @ts-expect-error fixture is worse than useless, because it passes only in the broken configuration and turns CI red once the types are fixed.

The gate deliberately does not cover runtime.types.ts — importing StyledProps pulls 22 source files into the program and surfaces nine pre-existing errors unrelated to this change. Enabling the flag repo-wide is a reasonable follow-up (22 errors across 13 files today) but a different PR.

npm pack --dry-run ships no __tests__; yarn build is unaffected, since bob's babel targets exclude __tests__ and its typescript target reads only the root tsconfig. One caveat: lefthook.yml's pre-commit runs bare yarn tsc, so the gate runs in CI but not on a local commit.

Full suite, typecheck and lint measured against a pristine-main baseline on the same machine — no new failures.

Notes

  • example/example-env.d.ts is left alone deliberately. It is a generated duplicate, already divergent from types.d.ts on main, and referenced by nothing.
  • feat(native): interop react-native-gesture-handler's Pressable and button family #416 already declares its new props in this form, so landing this first keeps types.d.ts internally consistent.
  • CONTRIBUTING.md asks that API changes start as an issue. Published types are API, so tell me if you would rather I open one — I led with the compile evidence because the change is small and the failure is mechanical.

Scope note. This sweep covers every prop declared in types.d.ts as of this branch. #416 adds
a react-native-gesture-handler module augmentation to the same file; its two props are already
declared | undefined, so the invariant holds across both, but the two PRs touch overlapping
lines and whichever lands second needs a rebase.

Under `exactOptionalPropertyTypes`, a bare `?: string` FORBIDS passing an
explicit `undefined` — and `className={condition ? 'a' : undefined}` is
exactly what a conditional class spreads. So every prop this package adds to
React Native's own interfaces rejected a value React Native's own optional
props accept, at every such call site.

Each added declaration is widened to `| undefined`. No runtime behaviour
changes; this is the declaration matching what the implementation already
allowed.
React Native's own optional props are declared `| undefined`; the props this
package augments them with are not. Under `exactOptionalPropertyTypes` that makes
`className={condition ? "p-4" : undefined}` — the ordinary conditional — a type
error, and there is no way for a consumer to fix it except by patching the
package.

`src/runtime.types.ts` needs the same treatment. Its three mapped types re-narrow
className to a bare optional, so `react-native-css/components` and `styled()` —
the imports the README documents — stay broken even once `types.d.ts` is fixed.

The contract is machine-checked. `src/__tests__/types` is a two-file program that
compiles the fixture against `types.d.ts` with the flag on; reverting `types.d.ts`
turns it red with 15 errors while the root typecheck stays green, which is the
point — the existing typecheck structurally cannot observe this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant