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, [, { theme: 'dark' }], [, { theme: 'blue', primaryColor: '#fff' }], [ThemeProvider, { theme: 'dark', primaryColor: '#fff', typo: 1 }], ]}
>
< />
</>;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:
| Message | What 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] tuple | a bare component whose props are not all optional |
first tuple element must be a component | the pair is the wrong way round, or the first entry is data |
second tuple element must be props, not another component | two components in one tuple |
not a valid provider | the 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' });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:
providersthat is not an array throws aTypeErrornaming 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
componentsprop with noprovidersthrows aTypeErrortelling 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
- API reference — every export, and the types this page named