Skip to Content
ComposeKeeping the Check

Keeping the check

@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 provides powerful compile-time prop validation, but that validation depends on how you declare your provider arrays. If you move your providers into a named constant or a wrapper component, you might accidentally “silence” the type checker.

The Danger of Type Widening

In TypeScript, when you declare an array, the compiler often “widens” the type. Instead of seeing a fixed-length tuple of specific components and props, it sees a general array of (Component | Props)[].

When a type is widened, ComposeProvider can no longer correlate a specific component with its specific props. As a result, the prop validation fails silently.

Maintaining Checks in Named Arrays

If you define your provider list as a constant, you must use as const. This tells TypeScript to treat the array as a read-only tuple with a fixed length and exact values, preserving the a-priori type information needed for validation.

import type { } from '@evanion/compose'; const : = [[, {}]]; const = [[, {}]] as ; const = [[, { : 'dark' }]]; // Compiled for their errors only: `constant` misses `theme`, and `plain` has // widened to an array that is not a provider. < providers={}>{null}</>;
Type 'readonly [readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, {}]]' is not assignable to type 'readonly [readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, {}]] & readonly [readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, PropsWithoutChildren<...>]]'. Type 'readonly [readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, {}]]' is not assignable to type 'readonly [readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, PropsWithoutChildren<({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element>]]'. Type 'readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, {}]' is not assignable to type 'readonly [({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element, PropsWithoutChildren<({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element>]'. Type at position 1 in source is not compatible with type at position 1 in target. Property 'theme' is missing in type '{}' but required in type 'PropsWithoutChildren<({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element>'.
< providers={}>{null}</>;
Type '((({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element) | { theme: string; })[][]' is not assignable to type 'ProviderArray'. Type '((({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element) | { theme: string; })[]' is not assignable to type 'Provider'. Type '((({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element) | { theme: string; })[]' is not assignable to type 'ComponentClass<any, any> | FunctionComponent<any>'.
const = ( < ={}> <>Brass: Birmingham</> </>, ); ; // -> '<div><p>Brass: Birmingham</p></div>'

Comparing Declaration Styles:

DeclarationValidation StatusResult
providers={[...]}, inlineâś… Fully CheckedEvery entry is validated.
const providers = [...] as constâś… Fully CheckedEvery entry is validated.
const providers = [...]❌ WidenedProp checks are lost; hand-written tuples are rejected.
const providers: ProviderArray = [...]❌ SilencedType is too general; reports no errors.
const providers = [...] satisfies ProviderArray❌ SilencedType is too general; reports no errors.

Pro Tip: Using the provider() helper avoids this entire problem. Because provider() checks props at the call site, the check is preserved regardless of how the resulting tuple is stored.

Preserving Checks in Wrapper Components

It is common to create a custom wrapper component (e.g., <AppProviders />) to encapsulate your root provider logic. However, if you type the providers prop simply as ProviderArray, you’ve silenced the check for anyone using your wrapper.

To fix this, use a const type parameter. This forces TypeScript to infer the exact, fixed-length tuple of the caller’s array.

import type { , } from '@evanion/compose'; function <const extends >({ , , }: <>) { return < ={}>{}</>; } // Compiled for its error only: the caller's tuple misses `theme`. < ={[, [, {}]]}>
Property 'theme' is missing in type '{}' but required in type 'PropsWithoutChildren<({ theme, children, }: PropsWithChildren<{ theme: "light" | "dark"; }>) => Element>'.
{null} </>; const = ( < ={[]}> <>Brass: Birmingham</> </>, ); ; // -> '<div id="cart"><p>Brass: Birmingham</p></div>'

How it Works:

  1. <const T extends ProviderArray>: The const modifier tells TypeScript to infer T as the most specific literal type possible (a fixed-length tuple).
  2. ComposeProviderProps<T>: By typing the props as ComposeProviderProps<T>, the validation logic of ComposeProvider is passed through to the caller of your wrapper.

Now, if a developer uses your <AppProviders /> and passes a ThemeProvider with missing props, they will get a compile error at the call site.

Every @evanion/compose export

The API reference lists every export of @evanion/compose, including ValidateProvider, the type that produces the error messages described in Where the check happens.

Last updated on