Skip to content

Bug: styled as-prop typings in v8 cause a ~23× TypeScript instantiation blow-up (OOM / very slow tsc) #1489

Description

@ArtuhovichVladislav

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions