Skip to Content
ComposeAPI Reference

API reference

@evanion/compose 2.1.0 and its patch releases up to 2.1.1, the version on npm. Values on this page were produced by running the source at 77650ee, a later commit than the 2.1.0 tag, and are not re-executed.

@evanion/compose exports two functions and eight types. Each one has a heading below.

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

ComposeProvider is a generic function. Its const T parameter makes TypeScript infer the array as a fixed-length array type, one type per position. A React.FC annotation would widen the array and drop the per-entry checks.

ComposeProvider returns a React fragment around the nested tree, so it adds no DOM element of its own. An entry that is a bare component is rendered with no props; a tuple entry is rendered with its second element spread as props. Below, CartProvider renders <div id="cart">, and ThemeProvider and CurrencyProvider each render a div whose id is their theme or currency prop. renderToStaticMarkup, from react-dom/server, turns the tree into the HTML string markup:

const = ( < ={[ // 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`. [, { : 'SEK' }], ]} > <>Brass: Birmingham</> </>, ); ; // -> '<div id="cart"><div id="dark"><div id="SEK"><p>Brass: Birmingham</p></div></div></div>'

ComposeProvider throws TypeError when providers is not an array, and when a components prop, the 1.x name for providers, is supplied without providers. In development it also warns once on an empty array.

A new props value on an entry re-renders that provider in place, and the state below it stays. A change to the array’s order or length remounts everything below the change.

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 [component, props] as passed, and the check runs only in the compiler. AnyComponent and PropsWithoutChildren<T> are defined below. T is inferred from component in the same expression, so props is checked and completed against that component. Below, ThemeProvider takes theme: 'light' | 'dark', and the call reports 'blue' where it is written:

const = (, { theme: 'blue' });
Type '"blue"' is not assignable to type '"light" | "dark"'.
; // -> [ThemeProvider, { theme: 'blue' }]

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

AnyComponent is the constraint a generic parameter uses to accept any component. ComponentType<unknown> would reject a component with concrete props, because component props are contravariant: a ComponentType<{ theme: string }> is not assignable to a ComponentType<unknown>. TypeScript has no “some component, props unknown” type for this position. Inference is unaffected: every check in @evanion/compose resolves against the component actually passed.

PropsWithoutChildren<T>

type PropsWithoutChildren<T extends AnyComponent> = Omit< ComponentProps<T>, 'children' >;
Signature — a shape, not a call

PropsWithoutChildren<T> is the props 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

Provider is the type of 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. A value annotated with this type has lost its length and the type at each position, so ComposeProvider checks none of its entries. Preserving Checks in Wrapper Components shows the wrapper that keeps the check.

ValidateProvider<T>

type ValidateProvider<T> = T extends readonly [infer C, infer P] ? C extends AnyComponent ? P extends AnyComponent ? ComposeError<'second tuple element must be props, not another component'> : Exclude<keyof P, keyof PropsWithoutChildren<C>> extends never ? [P] extends [PropsWithoutChildren<C>] ? T : readonly [C, PropsWithoutChildren<C>] : ComposeError<`unknown prop '${Extract< Exclude<keyof P, keyof PropsWithoutChildren<C>>, string >}'`> : ComposeError<'first tuple element must be a component'> : T extends AnyComponent ? Record<string, never> extends PropsWithoutChildren<T> ? T : ComposeError<'this component requires props — use provider(Component, props) or a [Component, props] tuple'> : ComposeError<'not a valid provider'>;
Signature — a shape, not a call

Checks one entry. A valid entry maps to itself. A failing one maps to ComposeError<"message">, a type naming what is wrong, except for a missing or mistyped prop value, which maps to the shape the entry should have had. ComposeError is internal: @evanion/compose does not export it. Debugging Compose Errors lists the messages.

ValidateProviders<T>

type ValidateProviders<T extends ProviderArray> = { readonly [K in keyof T]: ValidateProvider<T[K]>; };
Signature — a shape, not a call

ValidateProvider applied to each position of a fixed-length array type, so the error points at the offending entry.

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 true only when the array’s length is number, so it separates a widened array from a fixed-length array type. Gating on it does two things:

  • A widened ProviderArray passes through unchecked. Checking it would always fail, because a widened entry never matches the shape it should have had.
  • An array whose length is number is never compared member by member. That comparison makes TypeScript compare array methods such as every, map and reduce, and report a mismatch in a callback’s parameter type before it mentions the real one.

T stays in the intersection so TypeScript can infer T from the array you pass. Given ValidateProviders<T> alone, TypeScript infers no T from the array, falls back to ProviderArray, and checks nothing.

ComposeProviderProps<T>

interface ComposeProviderProps<T extends ProviderArray = ProviderArray> { providers: ValidatedProviders<T>; children: React.ReactNode; }
Signature — a shape, not a call

The props of ComposeProvider, exported for a wrapper that names them. The wrapper keeps the per-entry checks only when it declares its own <const T extends ProviderArray> and passes T here. Under the default T, providers is a plain ProviderArray. Preserving Checks in Wrapper Components shows one.

Migrating @evanion/compose from 1.x

1.0.82.0
<ComposeProvider components={[...]}><ComposeProvider providers={[...]}>
dual CommonJS and ESM buildsESM only; require('@evanion/compose') needs Node 20.19 or later on Node 20, or 22.12 or later
the Component typeProvider or ProviderArray
Provider, an alias of ComponentAnyComponent | readonly [AnyComponent, unknown]
ComposeProvider typed as a React.FCa generic function that no longer assigns to React.FC
a missing providers fails reading undefinedthrows a TypeError naming providers

@evanion/compose 2.0 removes the components prop from the type surface. A JavaScript caller that still passes it gets a named error. React.createElement passes the props an untyped caller would:

const = ({ }: React.) => ( < ="cart">{}</> ); // The props a JavaScript caller passes, which no compiler checked. const = { : [] } as never; let = ''; try { (React.(, )); } catch () { = ( as Error).; } ; // -> 'ComposeProvider: `components` was removed in v2.0 — rename it to `providers`.'
Last updated on