Skip to main content
Advertisement

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
Advertisement