16.3 타입 테스팅 — tsd와 expect-type으로 타입 자체를 단위 테스트
타입 테스트란?
TypeScript의 타입 시스템 자체를 테스트하는 것입니다. 유틸리티 타입 라이브러리, 제네릭 함수, 복잡한 타입 변환이 올바르게 작동하는지 검증합니다.
expect-type 라이브러리
npm install --save-dev expect-type
import { expectTypeOf } from 'expect-type'
// 기본 타입 검사
expectTypeOf(42).toBeNumber()
expectTypeOf('hello').toBeString()
expectTypeOf(true).toBeBoolean()
expectTypeOf(null).toBeNull()
expectTypeOf(undefined).toBeUndefined()
// 함수 타입 검사
function greet(name: string): string {
return `Hello, ${name}!`
}
expectTypeOf(greet).toBeFunction()
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>()
expectTypeOf(greet).returns.toBeString()
// 제네릭 함수 타입 검사
function identity<T>(value: T): T {
return value
}
expectTypeOf(identity(42)).toBeNumber()
expectTypeOf(identity('hello')).toBeString()
expectTypeOf(identity).toBeCallableWith(42)
유틸리티 타입 테스트
import { expectTypeOf } from 'expect-type'
import { describe, it } from 'vitest'
// 직접 만든 유틸리티 타입들
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('유틸리티 타입 테스트', () => {
it('DeepReadonly가 올바르게 동작한다', () => {
type ReadonlyUser = DeepReadonly<User>
// readonly 타입 확인
expectTypeOf<ReadonlyUser>().toEqualTypeOf<{
readonly id: string
readonly name: string
readonly email: string | null
readonly age?: number
}>()
})
it('NonNullableKeys가 nullable 키를 제외한다', () => {
type RequiredKeys = NonNullableKeys<User>
// id, name만 non-nullable (email은 null 포함, age는 optional)
expectTypeOf<RequiredKeys>().toEqualTypeOf<'id' | 'name'>()
})
})
API 응답 타입 테스트
import { expectTypeOf } from 'expect-type'
// API 클라이언트 타입
interface ApiResponse<T> {
data: T
status: number
message: string
}
type UnwrapApiResponse<T> = T extends ApiResponse<infer U> ? U : never
interface UserListResponse extends ApiResponse<User[]> {}
// 타입 추론 테스트
type UnwrappedUsers = UnwrapApiResponse<UserListResponse>
expectTypeOf<UnwrappedUsers>().toEqualTypeOf<User[]>()
// 함수 오버로드 타입 테스트
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 — 선언 파일 테스트
npm install --save-dev tsd
// index.test-d.ts
import { expectType, expectError, expectAssignable } from 'tsd'
import { createStore, Store } from './store'
// 정상 케이스
const store = createStore({ count: 0, name: '' })
expectType<Store<{ count: number; name: string }>>(store)
expectType<number>(store.get('count'))
expectType<string>(store.get('name'))
// 에러 케이스 — 타입 오류가 발생해야 함
expectError(store.get('invalid')) // 없는 키
expectError(store.set('count', 'abc')) // 잘못된 값 타입
// 할당 가능성 테스트
expectAssignable<{ count: number }>(store.getAll())
Vitest로 타입 테스트 통합
// types.test.ts
import { describe, it, expectTypeOf } from 'vitest'
// Vitest 4.x+에서 expectTypeOf 내장 지원
describe('타입 테스트 (Vitest)', () => {
it('배열 map 타입 추론', () => {
const numbers = [1, 2, 3]
const strings = numbers.map(String)
expectTypeOf(strings).toEqualTypeOf<string[]>()
expectTypeOf(strings[0]).toBeString()
})
it('Promise 타입 추론', 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('조건부 타입 검증', () => {
type IsArray<T> = T extends any[] ? true : false
expectTypeOf<IsArray<string[]>>().toEqualTypeOf<true>()
expectTypeOf<IsArray<string>>().toEqualTypeOf<false>()
expectTypeOf<IsArray<never>>().toEqualTypeOf<never>()
})
})
고수 팁
타입 에러를 의도적으로 확인하기
// @ts-expect-error를 활용한 타입 에러 테스트
function acceptString(value: string): void {}
// 올바른 사용
acceptString('hello')
// 타입 에러가 발생해야 하는 케이스
// @ts-expect-error — 아래 줄에 타입 에러가 없으면 컴파일 에러 발생
acceptString(42)
// @ts-ignore vs @ts-expect-error
// @ts-ignore: 에러가 없어도 조용히 무시
// @ts-expect-error: 에러가 없으면 경고 — 테스트에 더 적합
satisfies 연산자로 타입 검증
const config = {
host: 'localhost',
port: 3000,
ssl: false,
} satisfies {
host: string
port: number
ssl: boolean
}
// config.port는 number로 추론 (as const 없이도 정확한 타입 유지)
const port = config.port // number, not 3000