Skip to Content
ComposeWhere the Check Happens

Ahead of the release

npm install @evanion/compose gives you 2.0.0. These pages document main, which has changes that release does not.

Where the check happens

The concept. You have an array of entries — a bare component, a provider() call, a [component, props] tuple — and all three are checked against the component in them. This page is about the position that is not: the same tuple, assigned to a variable first, compiles with a missing prop and says nothing.

What you get. A reason to write the array where you use it, and the two annotations that buy the check back when you cannot.

Why you want it. The failure is silent. The props go unchecked, the app builds, and the provider renders with undefined where a colour was meant to be. Nothing in the type system points at the line; the line is the one where you named the array.

How the library gets you there. ComposeProvider checks each entry at the type level and does nothing at runtime beyond looking at whether providers is an array. So where you write the array decides whether the check runs at all.

Three positions compose reads differently

Inline in JSX

< ={[ ThemeProvider,
Type '({ children, }: React.PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => React.JSX.Element' is not assignable to type '(({ children, }: PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => Element) & ComposeError<"this component requires props — use provider(Component, props) or a [Component, props] tuple">'. Type '({ children, }: React.PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => React.JSX.Element' is not assignable to type 'ComposeError<"this component requires props — use provider(Component, props) or a [Component, props] tuple">'.
[, { theme: 'dark' }],
Type '{ theme: "dark"; }' is not assignable to type '{ readonly theme: "dark"; } & PropsWithoutChildren<({ children, }: PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => Element>'. Property 'primaryColor' is missing in type '{ theme: "dark"; }' but required in type 'PropsWithoutChildren<({ children, }: PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => Element>'.
[, { theme: 'blue', primaryColor: '#fff' }],
Type '"#fff"' is not assignable to type 'never'.
Type 'string' is not assignable to type 'never'.
[ThemeProvider, { theme: 'dark', primaryColor: '#fff', typo: 1 }],
Type '[({ children, }: PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => Element, { theme: "dark"; primaryColor: "#fff"; typo: 1; }]' is not assignable to type 'readonly [({ children, }: PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => Element, { readonly theme: "dark"; readonly primaryColor: "#fff"; readonly typo: 1; }] & ComposeError<...>'. Property '"ComposeError: unknown prop 'typo'"' is missing in type '[({ children, }: PropsWithChildren<{ theme: "light" | "dark"; primaryColor: string; }>) => Element, { theme: "dark"; primaryColor: "#fff"; typo: 1; }]' but required in type 'ComposeError<"unknown prop 'typo'">'.
]} > < /> </>;

A JSX attribute is a contextually typed position, so TypeScript infers the array as a literal tuple and checks every entry. Those diagnostics are the compiler’s own, produced when this page was built, and tools/repo-checks/src/doc-twoslash.test.ts holds them both ways round: an error this listing produces and does not declare fails the build, and so does a declared error the compiler has stopped reporting.

The wrong-value entry is the one that reads badly. It reports twice, against never, because the attribute position has already narrowed the tuple. Through provider() the same mistake reports once and names both sides — see reading a compose error.

Through provider()

const = [ , (, { : }), (, { : 'dark', : '#007acc' }), ];

provider() infers the component type from its own first argument, so the props object is checked at the call. That also means the editor knows the prop names while you are typing the object, which a bare tuple cannot offer.

In a variable, unannotated

const
const entry: (React.FC<ThemeProps> | { theme: string; })[]
entry
= [, { : 'dark' }];

The pairing between the component and its props is gone before ComposeProvider ever sees it — the element type is a union, not a tuple. Two ways out:

const = [ (, { : 'dark', : '#007acc' }), ]; const = [ [, { : 'dark', : '#007acc' }], ] satisfies ;

Reading a compose error

A failing entry has to resolve to some type your value is not assignable to, and whatever that type is ends up quoted in the diagnostic. For a failure that is not a wrong value, compose carries the explanation in a property key, so it reads as a missing property rather than as a structural walk you have to decode. The last diagnostic on the first listing of this page is that shape: Property '"ComposeError: unknown prop 'typo'"' is missing in type ….

The messages you can get:

MessageWhat it means
unknown prop 'x'the props object has a key the component does not declare
this component requires props — use provider(Component, props) or a [Component, props] tuplea bare component whose props are not all optional
first tuple element must be a componentthe pair is the wrong way round, or the first entry is data
second tuple element must be props, not another componenttwo components in one tuple
not a valid providerthe entry is neither a component nor a two-element tuple

A missing or mistyped prop value gets no carrier. It resolves to the shape the entry should have had, because the assignability message already says everything a message could:

(, { theme: 'blue', : '#fff' });
Type '"blue"' is not assignable to type '"light" | "dark"'.

Forwarding a compose array through a wrapper

A value already typed ProviderArray has lost the identity of its elements, so there is nothing left to check entry by entry. ValidatedProviders detects that and resolves to plain ProviderArray, which is what lets a wrapper component compile:

function ({ , , }: { : ; : React.; }) { return < ={}>{}</>; }

The entries are unchecked at that boundary. Build the array where the components are known if you want the per-entry errors, and pass the built array through.

Why compose checks for an excess key first

ValidateProvider tests for an unknown key before it tests assignability, and the order matters for a component whose props are all optional. Such a component is a weak type: an object of only unrelated keys is not assignable to it. Checking assignability first sends that case down the other branch, the one that resolves a failing entry to the shape it should have had. There, comparing your mutable tuple against a constructed readonly tuple makes TypeScript report the variance of Array.prototype.every rather than the prop nobody declared.

What compose does at runtime

Three things happen at runtime, and only three:

  • providers that is not an array throws a TypeError naming the prop and the type it received. Without it, a missing prop surfaces from the array reads as “Cannot read properties of undefined”, naming neither.
  • A components prop with no providers throws a TypeError telling you to rename it. That prop was removed in 2.0 and is not in the type surface, so only a caller outside TypeScript reaches this.
  • An empty array logs a development-mode warning, once per process.

Nothing else is inspected. provider() builds a tuple and returns it; the type parameters do the rest.

Where to go next

Last updated on