16.3 Type Testing — Unit Testing Types with tsd and expect-type
What is Type Testing?
Testing the TypeScript type system itself. Verifies that utility type libraries, generic functions, and complex type transformations work correctly.
expect-type Library
npm install --save-dev expect-type
import { expectTypeOf } from 'expect-type'
// Basic type checks
expectTypeOf(42).toBeNumber()
expectTypeOf('hello').toBeString()
expectTypeOf(true).toBeBoolean()
expectTypeOf(null).toBeNull()
expectTypeOf(undefined).toBeUndefined()
// Function type check
function greet(name: string): string {
return `Hello, ${name}!`
}
expectTypeOf(greet).toBeFunction()
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>()
expectTypeOf(greet).returns.toBeString()
// Generic function type check
function identity<T>(value: T): T {
return value
}
expectTypeOf(identity(42)).toBeNumber()
expectTypeOf(identity('hello')).toBeString()
expectTypeOf(identity).toBeCallableWith(42)
Utility Type Testing
import { expectTypeOf } from 'expect-type'
import { describe, it } from 'vitest'
// Custom utility types
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}
type NonNullableKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : null extends T[K] ? never : K
}[keyof T]
interface User {
id: string
name: string
email: string | null
age?: number
}
describe('Utility type tests', () => {
it('DeepReadonly works correctly', () => {
type ReadonlyUser = DeepReadonly<User>
// Verify readonly type
expectTypeOf<ReadonlyUser>().toEqualTypeOf<{
readonly id: string
readonly name: string
readonly email: string | null
readonly age?: number
}>()
})
it('NonNullableKeys excludes nullable keys', () => {
type RequiredKeys = NonNullableKeys<User>
// Only id, name are non-nullable (email includes null, age is optional)
expectTypeOf<RequiredKeys>().toEqualTypeOf<'id' | 'name'>()
})
})
API Response Type Testing
import { expectTypeOf } from 'expect-type'
// API client types
interface ApiResponse<T> {
data: T
status: number
message: string
}
type UnwrapApiResponse<T> = T extends ApiResponse<infer U> ? U : never
interface UserListResponse extends ApiResponse<User[]> {}
// Test type inference
type UnwrappedUsers = UnwrapApiResponse<UserListResponse>
expectTypeOf<UnwrappedUsers>().toEqualTypeOf<User[]>()
// Function overload type testing
function parseValue(value: string): number
function parseValue(value: number): string
function parseValue(value: string | number): number | string {
if (typeof value === 'string') return parseInt(value)
return value.toString()
}
expectTypeOf(parseValue('42')).toBeNumber()
expectTypeOf(parseValue(42)).toBeString()
tsd — Declaration File Testing
npm install --save-dev tsd
// index.test-d.ts
import { expectType, expectError, expectAssignable } from 'tsd'
import { createStore, Store } from './store'
// Happy path
const store = createStore({ count: 0, name: '' })
expectType<Store<{ count: number; name: string }>>(store)
expectType<number>(store.get('count'))
expectType<string>(store.get('name'))
// Error cases — these should cause type errors
expectError(store.get('invalid')) // Non-existent key
expectError(store.set('count', 'abc')) // Wrong value type
// Assignability test
expectAssignable<{ count: number }>(store.getAll())
Integrated Type Testing with Vitest
// types.test.ts
import { describe, it, expectTypeOf } from 'vitest'
// Vitest 4.x+ has built-in expectTypeOf support
describe('Type tests (Vitest)', () => {
it('array map type inference', () => {
const numbers = [1, 2, 3]
const strings = numbers.map(String)
expectTypeOf(strings).toEqualTypeOf<string[]>()
expectTypeOf(strings[0]).toBeString()
})
it('Promise type inference', async () => {
const asyncFn = async () => ({ id: 1, name: 'Alice' })
const result = await asyncFn()
expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>()
expectTypeOf(asyncFn).returns.resolves.toEqualTypeOf<{ id: number; name: string }>()
})
it('conditional type verification', () => {
type IsArray<T> = T extends any[] ? true : false
expectTypeOf<IsArray<string[]>>().toEqualTypeOf<true>()
expectTypeOf<IsArray<string>>().toEqualTypeOf<false>()
expectTypeOf<IsArray<never>>().toEqualTypeOf<never>()
})
})
Pro Tips
Intentionally Checking for Type Errors
// Test type errors using @ts-expect-error
function acceptString(value: string): void {}
// Valid usage
acceptString('hello')
// Cases where type error should occur
// @ts-expect-error — compile error if next line has NO type error
acceptString(42)
// @ts-ignore vs @ts-expect-error
// @ts-ignore: silently ignores even if no error
// @ts-expect-error: warns if no error — better for testing
Type Verification with satisfies Operator
const config = {
host: 'localhost',
port: 3000,
ssl: false,
} satisfies {
host: string
port: number
ssl: boolean
}
// config.port is inferred as number (accurate type maintained without as const)
const port = config.port // number, not 3000