Ahead of the release
npm install @evanion/compose gives you 2.0.0. These pages document main, which has changes that release does not.
API reference
Everything @evanion/compose exports, one heading per symbol. Nothing here
assumes you have read the pages before it;
Where the check happens is where the type
relationship is taught, and this page links to it rather than teaching it
again.
ComposeProvider
function ComposeProvider<const T extends ProviderArray>(
props: ComposeProviderProps<T>,
): React.ReactElement;| Prop | Type | Notes |
|---|---|---|
providers | ValidatedProviders<T> | Providers to compose; the first is outermost |
children | React.ReactNode | Wrapped by every provider |
A generic function rather than a React.FC. The const T parameter is what
infers the array as a literal tuple, and a React.FC annotation would widen it
and take the per-entry checking with it.
ComposeProvider renders a fragment containing the nested tree. An entry that
is a bare component is rendered with no props; a tuple entry is rendered with
its second element spread as props:
import type { } from 'react';
import { , } from '@evanion/compose';
const = ({ }: ) => (
< ="cart">{}</>
);
const = ({
,
,
}: <{ : 'light' | 'dark' }>) => (
< ={}>{}</>
);
const = ({ }: ) => (
< ="currency">{}</>
);
export function ({ }: ) {
return (
<
={[
,
(, { : 'dark' }),
,
]}
>
{}
</>
);
}ComposeProvider throws TypeError when providers is not an array, and when
a components prop is supplied without providers. In development it also
warns once on an empty array.
provider(component, props)
function provider<T extends AnyComponent, P extends PropsWithoutChildren<T>>(
component: T,
props: P & Record<Exclude<keyof P, keyof PropsWithoutChildren<T>>, never>,
): readonly [T, PropsWithoutChildren<T>];provider returns the [component, props] tuple, and nothing else happens at
runtime. T is inferred from component in the same expression, so props is
checked and completed against that component. It is one of three ways to write
an entry, beside a bare component and a plain tuple:
import type { } from 'react';
import { , } from '@evanion/compose';
const = ({ }: ) => (
< ="currency">{}</>
);
const = ({
,
,
}: <{ : 'light' | 'dark' }>) => (
< ={}>{}</>
);
export function ({ }: ) {
return (
<
={[
// A bare component, for a provider whose props are all optional.
,
// A `provider()` call, checked where it is written.
(, { : 'dark' }),
// A tuple, checked where the array reaches `ComposeProvider`.
[, { : 'light' }],
]}
>
{}
</>
);
}The Record<Exclude<…>, never> intersection is what rejects an excess key.
Plain assignability would not: TypeScript’s excess-property check fires on a
fresh object literal and is skipped for a props object that arrives through a
variable.
AnyComponent
type AnyComponent = ComponentType<any>;For use in a generic constraint. ComponentType<unknown> would reject a
component with concrete props — component props are contravariant, so a
ComponentType<{ theme: string }> is not assignable to a
ComponentType<unknown> — and TypeScript has no “some component, props
unknown” type for this position. Inference is unaffected: every check below
resolves against the component actually passed.
PropsWithoutChildren<T>
type PropsWithoutChildren<T extends AnyComponent> = Omit<
ComponentProps<T>,
'children'
>;What an entry has to supply. children is excluded because ComposeProvider
provides it: the next provider down, or the caller’s own children at the
innermost level.
Provider
type Provider = AnyComponent | readonly [AnyComponent, unknown];One entry of the array: a component, or a component paired with its props.
ProviderArray
type ProviderArray = readonly Provider[];readonly, so an array written as const is assignable. Annotating a value
with this type widens away its element identity — see
forwarding a compose array through a wrapper.
ValidateProvider<T>
Checks one entry. A valid entry maps to itself; a failing one maps to a carrier type naming what is wrong, except for a missing or mistyped prop value, which maps to the shape the entry should have had. Reading a compose error lists the carriers.
ValidateProviders<T>
ValidateProvider applied across a tuple, positions preserved, so the error
points at the offending element.
ValidatedProviders<T>
type ValidatedProviders<T extends ProviderArray> = number extends T['length']
? ProviderArray
: T & ValidateProviders<T>;The type of the providers prop. number extends T['length'] is the standard
tuple-versus-array test, and gating on it does two things: a widened
ProviderArray is passed through unchecked instead of being compared against a
repaired shape it cannot match, and a non-tuple array is never compared member
by member — which would make TypeScript walk every, map and reduce and
report the variance of a predicate before mentioning the real mismatch.
T stays in the intersection because a bare ValidateProviders<T> is a
non-homomorphic mapped type and therefore not an inference site: TypeScript
would fall back to the constraint and check nothing.
ComposeProviderProps<T>
The props interface above, exported for a wrapper that needs to name it.
Migrating from 1.x
| Before | After |
|---|---|
<ComposeProvider components={[...]}> | <ComposeProvider providers={[...]}> |
LegacyComposeProviderProps | removed |
AnyComposeProviderProps | removed |
forwarding a ProviderArray variable failed to compile | compiles, unchecked at that boundary |
components is gone from the type surface entirely rather than deprecated. A
JavaScript caller that still passes it gets a named error:
TypeError: ComposeProvider: `components` was removed in v2.0 — rename it to `providers`.Keeping it as a second overload is what produced the unreadable diagnostics in
1.x: every error on a providers call carried a second half about the legacy
overload that could never match.