TypeScript Testing Types
Test TypeScript types at compile time using expect-type, tsd, and vitest type matchers
When to Use
- Verifying that utility types produce the expected output types
- Testing that generic functions infer types correctly
- Ensuring library public APIs maintain correct type signatures
- Catching type regressions in CI without runtime tests
Instructions
- Use
expectTypeOffrom vitest (built-in, no extra dependencies):
import { expectTypeOf, test } from 'vitest';
test('User type has correct shape', () => {
expectTypeOf<User>().toMatchTypeOf<{
id: string;
name: string;
email: string;
}>();
});
- Test function return types:
test('createUser returns User', () => {
expectTypeOf(createUser).returns.toMatchTypeOf<User>();
expectTypeOf(createUser).parameters.toEqualTypeOf<[name: string, email: string]>();
});
- Test generic inference:
test('identity preserves literal types', () => {
const result = identity('hello');
expectTypeOf(result).toEqualTypeOf<'hello'>();
});
- Verify types are NOT assignable:
test('UserId and PostId are not interchangeable', () => {
expectTypeOf<UserId>().not.toEqualTypeOf<PostId>();
expectTypeOf<UserId>().not.toMatchTypeOf<PostId>();
});
- Use
expect-typelibrary for standalone type tests:
import { expectTypeOf } from 'expect-type';
// In a .test-d.ts file or regular test file
expectTypeOf<Pick<User, 'id' | 'name'>>().toEqualTypeOf<{
id: string;
name: string;
}>();
expectTypeOf<Partial<User>>().toMatchTypeOf<{ id?: string }>();
- Use
tsdfor declaration file testing:
// test-d.ts
import { expectType, expectError } from 'tsd';
import { createUser } from './index';
expectType<User>(createUser('Alice', 'alice@test.com'));
expectError(createUser(123)); // Should fail with number input
Run with npx tsd in package.json.
- Test conditional types:
test('UnwrapPromise extracts inner type', () => {
expectTypeOf<UnwrapPromise<Promise<string>>>().toEqualTypeOf<string>();
expectTypeOf<UnwrapPromise<Promise<Promise<number>>>>().toEqualTypeOf<number>();
expectTypeOf<UnwrapPromise<string>>().toEqualTypeOf<string>();
});
- Test mapped types:
test('Nullable adds null to all properties', () => {
type Input = { name: string; age: number };
type Expected = { name: string | null; age: number | null };
expectTypeOf<Nullable<Input>>().toEqualTypeOf<Expected>();
});
- Compile-time only assertions (no runtime, no test runner):
// types.test.ts — checked by tsc, not executed
type Assert<T extends true> = T;
type IsEqual<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
type _test1 = Assert<IsEqual<ReturnType<typeof getUser>, Promise<User>>>;
type _test2 = Assert<IsEqual<Parameters<typeof createUser>, [string, string]>>;
Details
Type tests verify that your type-level code (generics, utility types, conditional types) produces the expected types. They catch regressions that runtime tests cannot — a function might work correctly at runtime but accept the wrong types due to an overly permissive generic.
toEqualTypeOf vs toMatchTypeOf:
toEqualTypeOf— exact match.{ a: string }does NOT equal{ a: string; b?: number }toMatchTypeOf— structural compatibility.{ a: string; b: number }MATCHES{ a: string }
Tool comparison:
- Vitest
expectTypeOf— integrated into vitest, runs alongside runtime tests, best for projects already using vitest expect-type— standalone library, works with any test runner, same API as vitest's built-intsd— designed for library authors testing .d.ts files, uses a different assertion API
When to test types:
- Utility types that transform other types (DeepPartial, NonNullable variants)
- Generic functions with complex inference
- Public API surfaces of libraries
- Discriminated union handlers (ensure exhaustiveness)
When NOT to test types:
- Simple type annotations (TypeScript already checks these)
- One-off types used in a single file
- Types that mirror a runtime test's assertions
Trade-offs:
- Type tests add maintenance overhead — but catch regressions that runtime tests miss
expectTypeOfassertions run at compile time, not runtime — test files must be type-checked by tsc- Complex type assertions can produce cryptic error messages
- Type tests do not verify runtime behavior — always pair with runtime tests for critical paths
Source
https://typescriptlang.org/docs/handbook/2/types-from-types.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.