Types can regress silently. Add type-level tests to catch breaks when refactoring generics, overloads, and public APIs.
tsd or expectTypeOf
// vitest + expectTypeOf
import { expectTypeOf, describe, it } from 'vitest';
type Foo<T> = { value: T };
describe('Foo', () => {
it('preserves generic', () => {
const x: Foo<number> = { value: 42 };
expectTypeOf(x.value).toEqualTypeOf<number>();
});
});CI and Type Coverage
- Run type tests in CI just like unit tests.
- Track “type coverage” budgets: disallow
any, enforcenoUncheckedIndexedAccess. - Gate releases of component libraries on passing type assertions.
Testing Polymorphic Components
import { expectTypeOf, it } from 'vitest';
// Polymorphic Button with `as` prop
declare function Button<C extends React.ElementType = 'button'>(props: {
as?: C;
} & React.ComponentPropsWithoutRef<C>): React.ReactNode;
it('infers correct props for anchor', () => {
const el = <Button as="a" href="/" />;
expectTypeOf(el.props.href).toBeString(); // href must exist on anchor
});Higher Order Component Props Preservation
function withLogging<P>(Comp: React.ComponentType<P>) {
return (props: P) => <Comp {...props} />;
}
type InputProps = { value: string; onChange: (v: string) => void };
const Input: React.FC<InputProps> = () => null;
const LoggedInput = withLogging(Input);
// ensure HOC preserves prop types
expectTypeOf<Parameters<typeof LoggedInput>[0]>().toEqualTypeOf<InputProps>();tsd Setup (Alternative)
// package.json
{
"scripts": {
"test:types": "tsd"
}
}// tsd.config.json
{
"entry": "./types/**/*.test-d.ts"
}// types/button.test-d.ts
import { expectType } from 'tsd';
import { Button } from '../dist';
expectType<JSX.Element>(<Button />);Guarding Public APIs in Libraries
- Export types (
export type { ButtonProps }) and assert them intest:types. - Lock down overloads and generic defaults with targeted assertions.
Snapshot Public Component Types
Lock down your public API with type snapshots using tsd or expectTypeOf.
// tsd: Button and TextField public types
import type { ComponentProps } from 'react';
import { expectType } from 'tsd';
import { Button } from '../dist';
import { TextField } from '../dist';
// Button supports as="a" | "button" and mirrors intrinsic attrs
type ButtonProps = ComponentProps<typeof Button>;
expectType<ButtonProps>({ as: 'button', onClick: () => {} });
expectType<ButtonProps>({ as: 'a', href: '/home' });
// @ts-expect-error - anchors need href
expectType<ButtonProps>({ as: 'a' });
// TextField narrows onChange to the correct event type
type TextFieldProps = ComponentProps<typeof TextField>;
expectType<TextFieldProps>({ label: 'Name', value: '', onChange: (e) => e.target.value });
expectType<TextFieldProps>({
as: 'textarea',
label: 'Bio',
defaultValue: '',
onChange: (e) => e.target.value,
});
// @ts-expect-error - wrong event type for textarea
expectType<TextFieldProps>({
as: 'textarea',
label: 'Bio',
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {},
});