Skip to Content
ComposeAPI Reference

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;
Signature — a shape, not a call
PropTypeNotes
providersValidatedProviders<T>Providers to compose; the first is outermost
childrenReact.ReactNodeWrapped 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>];
Signature — a shape, not a call

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>;
Signature — a shape, not a call

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' >;
Signature — a shape, not a call

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];
Signature — a shape, not a call

One entry of the array: a component, or a component paired with its props.

ProviderArray

type ProviderArray = readonly Provider[];
Signature — a shape, not a call

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>;
Signature — a shape, not a call

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

BeforeAfter
<ComposeProvider components={[...]}><ComposeProvider providers={[...]}>
LegacyComposeProviderPropsremoved
AnyComposeProviderPropsremoved
forwarding a ProviderArray variable failed to compilecompiles, 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.

Last updated on