Repository navigation
fix(types): accept an explicit undefined wherever a consumer writes an optional member - #418
YevheniiKotyrlo wants to merge 1 commit into
Conversation
|
| errors in the file | |
|---|---|
main (2421ea77) |
three TS2769: No overload matches this call, one per element |
this PR, merged into main |
none |
A mapping entry left undefined — main against this PR
The snippet from the body, rendered through the repo's jest with a window.dispatchEvent stub, so React's report of the error it recovered from reaches console.error:
| errors | accessibilityLabel |
style |
|
|---|---|---|---|
main (eda2b286) |
There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root., caused by Cannot read properties of undefined (reading 'split') |
#00f |
{} |
| this PR | none | #00f |
{ backgroundColor: "#f00" } |
Type-check cost — main against this PR
tsc --noEmit --extendedDiagnostics on clean checkouts of main and of this head merged into it, each started at high priority on the same sixteen logical processors; the counts are the same on every run.
main (2421ea77) |
this PR | |
|---|---|---|
| this repository: types | 167,998 | 171,599 |
| this repository: instantiations | 979,505 | 954,279 |
a file importing styled: instantiations |
480,167 | 479,980 |
the same file with 13 styled() mappings over React Native's components: instantiations |
936,529 | 585,414 |
| each mapping, on average | 35,105 | 8,110 |
three styled() mappings over Reanimated's Animated.View, Animated.Text and Animated.ScrollView: instantiations |
23,011,189 | 19,617,724 |
| the same file: errors | 2 × TS2589, 1 × TS2590 | the same errors |
Check times varied more between runs of one side than between the sides, so the table gives only the counts.
The mapping type no longer resolves its target: its keys are React Native's style keys, which an editor completes, beside any string.
Windows 11 Pro 10.0.26300, AMD Ryzen 9 9950X3D, Node 26.8.1, TypeScript 6.0.3.
6c6da94 to
d10ad90
Compare
8c8919f to
49ff1c9
Compare
…n option
Under exactOptionalPropertyTypes a bare `?: T` rejects the explicit
undefined React Native's own optional props accept, so
`className={condition ? "p-4" : undefined}` did not compile. Every
optional member a consumer writes now takes `| undefined`: the props
types.d.ts adds, runtime.types.ts, the options of the compiler, Metro,
the jest helper and useNativeCss, and what the exports' parameters
reach. `InlineStyleRecord`'s `s?: never` stays, marking a key that must
stay absent.
The props a styled() mapping adds come from one StyledSourceProps type.
A mapping's keys are the style keys the compiler collects, so its type
no longer resolves the target. styled() drops an entry left undefined
and fails with its Invalid mapping error for any value it cannot map.
optional-members.test.ts walks, with the type checker and the flag on,
what a consumer writes to each entry point package.json exports, and
fails on any optional member that rejects undefined.
e283081 to
157684a
Compare
Problem
React Native declares its own optional props as
?: T | undefined, while the props this package adds are bare?: string. UnderexactOptionalPropertyTypesthe two differ, and the difference falls on the most ordinary thing a consumer writes:styled()has the same bare members: theclassNameits unmapped overload adds,passThrough, and a mapping'snativeStyleMapping; so do the options a consumer writes for the compiler, Metro, the jest helper anduseNativeCss. A consumer's own declaration that widens such a prop takes effect only when it comes before this package's in the program.A mapping entry left
undefinedis also a runtime error:mappingToConfigkeeps it, andnativeStyleMappingcallspath.split(".")on it:A
nullorfalseentry, which the types reject but JavaScript can pass, fails the same way, and anullmapping or a stringnativeStyleMappingfails with aTypeErroror maps each character.Solution
types.d.tsandruntime.types.ts, of the compiler's, Metro's, the jest helper's anduseNativeCss's options, and of the reactivityEffectan observable'sget()takes, takes| undefined, as React Native writes its own, exceptInlineStyleRecord'ss?: never, which marks a key that must stay absent; the compiler's feature flags take it too.registerCSSandcompileWithAutoDebugtake one option type, which leaves outlogger, which the helper always sets itself, and letsregisterCSSpass on thedebug: "verbose"it already honours.styled()mapping adds come from oneStyledSourcePropstype, whichStyledProps,StyledReactElementandStyledComponentuse, and which the unmapped overload uses withDefaultStyledConfiguration, the default mappingsrc/native/api.tsxandsrc/web/api.tsxboth pass. They are writable, as the component's own props are, wheremainmade the mapped ones read-only.-rn-declaration, so any string is a key. The type no longer resolves the mapping's target, and an entry may be leftundefined, whichmappingToConfigdrops. On native, any other value it cannot map, an entry, anativeStyleMappingor the mapping itself, fails with anInvalid mappingerror that names it whenstyled()oruseCssElementreads the mapping.inlineVariables.excludetakes a readonly list, whichcompile()only reads.Tests
src/__tests__/types/optional-members.test.tschecks what a consumer writes with the type checker, the flag on: the propstypes.d.tsadds andruntime.types.ts, read whole, and what each entry point ofpackage.json'sexportstakes: a function's parameters and type constraints, what a component or function it returns takes, and an instance's methods. It leaves what a consumer only reads, such as a callback's arguments, another package's mapped types,native-internal, whose caller is the generated injection code, and the compiled stylesheetcompile()writes, which a consumer only passes back. It fails on any optional member that rejectsundefined, naming each place once: a member under the type that declares it, and one a mapped type such asPartialmakes optional, or gives a type withoutundefined, where the mapped type is applied. It reads every member a consumer reaches from those types, through call and construct signatures (their type parameters' constraints, parameters and return types), a class's constructor, index signatures, base types, unions, intersections, tuples and a generic's arguments, each type once, a recursive alias included; a generic declaration is read as written. Its fixtures hold a case for each reach and each form, beside the ones that pass. Onmainit names 41 places an optional member rejectsundefined; here none.src/__tests__/native/_styled-types.tsx: a mapping takes any style key, a-rn-declaration's among them, and an entry leftundefined, rejects a value naming no prop, and the class props stay writable; onmaintscfails theundefinedentry and the writable mapped prop.src/__tests__/types/styled.test.ts: the editor completes React Native's style keys undertarget: falseand under a string target; both fail onmain.src/__tests__/types/compiler-options.test.ts:inlineVariables.excludetakes a list declaredas const;mainfails it with TS4104.src/__tests__/native/styled.test.ios.tsx: a mapping entry set toundefinedmaps nothing, and any other value it cannot map, an entry, anativeStyleMappingor the mapping itself, fails atstyled()with an error that names it. Onmainthe first failstscand its render throws, and the others reach the render or throw aTypeError.DEVELOPMENT.mdand the repo'sadd-testandtriageskills list the test domains,types/and the Node tooling tests among them, andadd-testsays how each runs:yarn typecheckfor a casetscalone decides,node --testafter a build, andmaestro testfor a flow.Verification
Every
ci.ymljob passes on this head with #477's harness fix merged in: lint, typecheck, the tests (1392 passed), both builds, the web export, nothing unstaged, commitlint, and the iOS dev build on macOS 27.0.1 with Xcode 27.0, where CI pins 26.6.A
styled()mapping with anativeStyleMappingnow costs the type checker about 8,100 instantiations against 35,100 onmain, where one without costs about 8,000 on both, andtsc --noEmitover this repository makes 954,279 type instantiations against 979,505. The runs, the snippet above and theclassName={maybe}check onmainand on this branch are in this comment.Known limits
expo/typesdeclaresclassNameonViewProps,TextPropsand other React Native props without| undefined, andexpo-env.d.tsloads it before this package's declarations, so thereclassName={maybe}still fails until Expo's declarations widen too.main, since a-rn-declaration can add any key to the style.className?: stringnow fails, as spreading React Native's own| undefinedprops into one already does.styled()over Reanimated's animated components still exceeds the type checker's depth (TS2589, TS2590), as onmain, at about 15% fewer instantiations.CompilerOptions.loggerandConfig.nativeStyleMappingclears the 6 othersmainhas.Merge order
dir?: "ltr" | "rtl"toViewPropsandTextProps, and feat(metro): surface compiler warnings during a build #430 and fix(compiler): recover from a rule lightningcss cannot parse #493 addwarnings?: WarningLevelto Metro's options. Whichever lands second widens them, andoptional-members.test.tsfails until it does.exclusionListinsrc/compiler/compiler.ts; whichever lands second keeps it a readonly list, sinceinlineVariables.excludetakes one here.nativeStyleMappingdeclaration insrc/runtime.types.ts, and fix(native): give a target:false config its own scratch space, and never map onto the cached props #435 makesConfig's members inuseNativeCss.tsread-only; whichever lands second keeps both changes,| undefinedincluded.src/native/api.tsxandsrc/web/api.tsx.