Environment
| Environment |
Details |
| @linaria/react |
8.1.0 (regression introduced in 8.0.0; 7.0.1 is not affected) |
| typescript |
5.9.2 |
| react / react-dom |
19.2.5 |
| bundler |
Next.js 16.2.6 (Turbopack) via @wyw-in-js/nextjs |
| project size |
173 627 LoC, 376 files using styled, 956 styled usages |
Description
After upgrading @linaria/react from 7.0.1 → 8.1.0 (nothing else changed — same @wyw-in-js engine, same source), the TypeScript check exploded: cold tsc --noEmit went from ~9s to ~100s, and next build's in-build "Running TypeScript" step started failing with FATAL ERROR: Reached heap limit — JavaScript heap out of memory on CI containers with the default Node heap.
tsc --noEmit --extendedDiagnostics, cold (no .tsbuildinfo), same project, only @linaria/react's types/styled.d.ts differing:
| metric |
v8.1.0 (as published) |
with the fix below |
| Instantiations |
33 549 681 |
1 445 516 |
| Types |
753 933 |
295 074 |
| Memory used |
~12.4 GB |
~1.6 GB |
| Check time |
101.9 s |
10.0 s |
| Total time |
105.4 s |
13.6 s |
So a type-only change to styled.d.ts accounts for ~23× the instantiations, ~8× the memory and ~10× the check time. @linaria/react@7.0.1 (which already runs on @wyw-in-js) does not have this — its cold type-check is ~9s, matching the fixed v8 above — which localizes the regression to the v8 styled typings rather than the engine.
Root cause
types/styled.d.ts in v8 types the returned component as StyledComponentWithAs<TProps> with a call signature polymorphic over as:
type StyledComponentWithAs<TProps> = {
<TAs extends keyof IntrinsicElements>(props: TProps & {
as: TAs;
} & IntrinsicElements[TAs], context?: any): ReturnType<React.FunctionComponent>;
(props: TProps & { as?: React.ElementType }, context?: any): ReturnType<React.FunctionComponent<TProps>>;
// + contextTypes/defaultProps/displayName/propTypes helpers
};
The <TAs extends keyof IntrinsicElements> overload makes every styled component intersect its props with IntrinsicElements[TAs] across the whole set of intrinsic elements. Multiplied by hundreds of styled components (and React 19's JSX.IntrinsicElements surface), this yields the tens-of-millions of instantiations above.
Expected behavior
Bumping to v8 / adding a styled component should not multiply type-check cost by an order of magnitude.
Proposed fix
Flatten StyledComponentWithAs to a plain function component that still accepts as:
-type StyledComponentWithAs<TProps> = {
- <TAs extends keyof IntrinsicElements>(props: TProps & { as: TAs } & IntrinsicElements[TAs], context?: any): ReturnType<React.FunctionComponent>;
- (props: TProps & { as?: React.ElementType }, context?: any): ReturnType<React.FunctionComponent<TProps>>;
- contextTypes?: FunctionComponentStatic<TProps, 'contextTypes'>;
- defaultProps?: FunctionComponentStatic<TProps & { as?: React.ElementType }, 'defaultProps'>;
- displayName?: FunctionComponentStatic<TProps, 'displayName'>;
- propTypes?: FunctionComponentStatic<TProps & { as?: React.ElementType }, 'propTypes'>;
-};
+type StyledComponentWithAs<TProps> = React.FunctionComponent<TProps & {
+ as?: React.ElementType;
+}>;
Restores the right-hand column (1.45M instantiations, ~1.6 GB, ~10s), .d.ts-only, no runtime/CSS impact. Trade-off: as no longer narrows props to the chosen intrinsic element (becomes React.ElementType); a more surgical typing that keeps narrowing without the full polymorphic expansion would be ideal.
Reproducible Demo
Reproducible in any large styled-heavy project via tsc --extendedDiagnostics — Instantiations scales with the number of styled components.
Environment
Description
After upgrading
@linaria/reactfrom 7.0.1 → 8.1.0 (nothing else changed — same@wyw-in-jsengine, same source), the TypeScript check exploded: coldtsc --noEmitwent from ~9s to ~100s, andnext build's in-build "Running TypeScript" step started failing withFATAL ERROR: Reached heap limit — JavaScript heap out of memoryon CI containers with the default Node heap.tsc --noEmit --extendedDiagnostics, cold (no.tsbuildinfo), same project, only@linaria/react'stypes/styled.d.tsdiffering:So a type-only change to
styled.d.tsaccounts for ~23× the instantiations, ~8× the memory and ~10× the check time.@linaria/react@7.0.1(which already runs on@wyw-in-js) does not have this — its cold type-check is ~9s, matching the fixed v8 above — which localizes the regression to the v8styledtypings rather than the engine.Root cause
types/styled.d.tsin v8 types the returned component asStyledComponentWithAs<TProps>with a call signature polymorphic overas:The
<TAs extends keyof IntrinsicElements>overload makes everystyledcomponent intersect its props withIntrinsicElements[TAs]across the whole set of intrinsic elements. Multiplied by hundreds ofstyledcomponents (and React 19'sJSX.IntrinsicElementssurface), this yields the tens-of-millions of instantiations above.Expected behavior
Bumping to v8 / adding a
styledcomponent should not multiply type-check cost by an order of magnitude.Proposed fix
Flatten
StyledComponentWithAsto a plain function component that still acceptsas:Restores the right-hand column (1.45M instantiations, ~1.6 GB, ~10s),
.d.ts-only, no runtime/CSS impact. Trade-off:asno longer narrows props to the chosen intrinsic element (becomesReact.ElementType); a more surgical typing that keeps narrowing without the full polymorphic expansion would be ideal.Reproducible Demo
Reproducible in any large styled-heavy project via tsc --extendedDiagnostics — Instantiations scales with the number of styled components.