- Authors

- Name
- Youngju Kim
- @fjvbn20031
TypeScript 완전 정복: 타입 시스템부터 고급 패턴까지 실전 가이드
TypeScript는 JavaScript에 정적 타입 시스템을 추가한 언어입니다. 2026년 현재 프론트엔드와 백엔드를 막론하고 대부분의 대규모 프로젝트에서 TypeScript를 표준으로 채택하고 있습니다. 이 가이드에서는 기초부터 고급 패턴까지 실무에서 바로 활용할 수 있는 내용을 다룹니다.
1. TypeScript 타입 시스템 기초
1.1 원시 타입 (Primitive Types)
TypeScript의 원시 타입은 JavaScript의 것과 동일하지만 명시적으로 선언합니다.
// 기본 원시 타입
const name: string = '홍길동'
const age: number = 30
const isActive: boolean = true
const nothing: null = null
const undef: undefined = undefined
// ES6+ 타입
const sym: symbol = Symbol('unique')
const big: bigint = 9007199254740991n
// 타입 추론 - 대입 시 TypeScript가 자동으로 타입을 추론
const inferredName = '홍길동' // string으로 추론
const inferredAge = 30 // number로 추론
1.2 배열과 튜플
// 배열 타입 표기 방법 (두 가지 동일)
const nums1: number[] = [1, 2, 3]
const nums2: Array<number> = [1, 2, 3]
// 읽기 전용 배열
const readonlyNums: readonly number[] = [1, 2, 3]
// readonlyNums.push(4) // Error: push does not exist on readonly
// 튜플 - 길이와 각 위치의 타입이 고정
const pair: [string, number] = ['Alice', 30]
const triple: [string, number, boolean] = ['Bob', 25, true]
// 선택적 튜플 요소
const optional: [string, number?] = ['Charlie']
// 나머지 튜플
const rest: [string, ...number[]] = ['start', 1, 2, 3, 4]
1.3 Enum
// 숫자 열거형 (기본: 0부터 시작)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 문자열 열거형
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE',
}
// const enum - 컴파일 시 인라인으로 대체되어 번들 크기 최적화
const enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING',
}
// const enum 사용 시 컴파일 결과: 'ACTIVE' 문자열로 직접 치환
const currentStatus = Status.Active // 컴파일 후: 'ACTIVE'
// 일반 enum vs const enum 선택 기준:
// - 런타임에 enum 객체를 순회할 필요가 있다면 일반 enum
// - 성능 최적화가 중요하고 객체 순회가 불필요하면 const enum
1.4 any vs unknown vs never vs void
이 네 가지는 TypeScript에서 가장 혼동하기 쉬운 특수 타입입니다.
// any - 타입 검사를 완전히 비활성화 (사용 자제 권장)
let anyVal: any = '문자열'
anyVal = 42 // OK
anyVal = true // OK
anyVal.nonExistent() // 런타임 오류 가능, 컴파일 오류 없음
// unknown - any보다 안전한 대안
let unknownVal: unknown = '문자열'
// unknownVal.toUpperCase() // Error: 타입 검사 필요
if (typeof unknownVal === 'string') {
unknownVal.toUpperCase() // OK: 타입 가드 후 사용 가능
}
// never - 절대 발생하지 않는 값의 타입
function throwError(msg: string): never {
throw new Error(msg)
}
function infiniteLoop(): never {
while (true) {}
}
// never는 완전성 검사에 유용
type Shape = 'circle' | 'square' | 'triangle'
function area(shape: Shape): number {
switch (shape) {
case 'circle':
return Math.PI * 1
case 'square':
return 1
case 'triangle':
return 0.5
default:
// shape가 never 타입이어야 함 - 새로운 Shape 추가 시 컴파일 오류 발생
const _exhaustive: never = shape
throw new Error(`Unknown shape: ${_exhaustive}`)
}
}
// void - 반환값이 없는 함수
function logMessage(msg: string): void {
console.log(msg)
// return undefined // OK
// return 'value' // Error
}
1.5 Type Alias vs Interface
// Type Alias
type Point = {
x: number
y: number
}
type ID = string | number
// Interface
interface IPoint {
x: number
y: number
}
// 주요 차이점:
// 1. Interface는 선언 병합(Declaration Merging) 가능
interface IPoint {
z?: number // 기존 IPoint에 z 추가됨
}
// 2. Type Alias는 Union/Intersection/Primitive 등 모든 타입에 사용 가능
type StringOrNumber = string | number
type WithTimestamp = Point & { createdAt: Date }
// 3. Interface는 class가 implements 가능 (Type도 가능)
class MyPoint implements IPoint {
x = 0
y = 0
}
// 선택 기준:
// - 라이브러리/SDK 공개 API: Interface (선언 병합으로 확장 용이)
// - 앱 내부 타입: Type Alias (더 유연)
// - 객체 형태만 표현: 둘 다 가능
1.6 Literal Types & Template Literal Types
// 리터럴 타입
type Direction = 'north' | 'south' | 'east' | 'west'
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6
const move = (dir: Direction) => console.log(dir)
move('north') // OK
// move('up') // Error
// 템플릿 리터럴 타입 (TypeScript 4.1+)
type EventName = 'click' | 'focus' | 'blur'
type Handler = `on${Capitalize<EventName>}`
// 결과: 'onClick' | 'onFocus' | 'onBlur'
type CSSUnit = 'px' | 'em' | 'rem' | 'vw' | 'vh'
type CSSValue = `${number}${CSSUnit}`
// 예: '16px', '1.5em', '100vw' 등
// 실용 예시: API 엔드포인트 타입
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
type APIRoute = `${HttpMethod} ${Endpoint}`
// 결과: 'GET /users' | 'GET /posts' | ... 등 12가지 조합
2. 객체 타입과 유틸리티 타입
2.1 객체 타입 고급 기능
interface User {
readonly id: number // 읽기 전용
name: string
email?: string // 선택적 속성
[key: string]: unknown // 인덱스 시그니처
}
const user: User = { id: 1, name: 'Alice' }
// user.id = 2 // Error: readonly
// 중첩 readonly
interface DeepReadonly {
readonly settings: {
readonly theme: string
readonly language: string
}
}
2.2 내장 유틸리티 타입
interface Todo {
id: number
title: string
completed: boolean
description: string
}
// Partial<T> - 모든 속성을 선택적으로
type PartialTodo = Partial<Todo>
// { id?: number; title?: string; completed?: boolean; description?: string }
// Required<T> - 모든 속성을 필수로
type RequiredTodo = Required<Partial<Todo>>
// Readonly<T> - 모든 속성을 읽기 전용으로
type ReadonlyTodo = Readonly<Todo>
// Pick<T, K> - 특정 속성만 선택
type TodoPreview = Pick<Todo, 'id' | 'title'>
// Omit<T, K> - 특정 속성 제외
type TodoWithoutDescription = Omit<Todo, 'description'>
// Record<K, V> - 키-값 맵 타입
type TodoMap = Record<number, Todo>
const todos: TodoMap = {
1: { id: 1, title: 'Learn TypeScript', completed: false, description: '' },
}
// Exclude<T, U> - Union에서 U에 해당하는 타입 제거
type T1 = Exclude<string | number | boolean, boolean> // string | number
// Extract<T, U> - Union에서 U에 해당하는 타입만 추출
type T2 = Extract<string | number | boolean, string | boolean> // string | boolean
// NonNullable<T> - null과 undefined 제거
type T3 = NonNullable<string | null | undefined> // string
// ReturnType<T> - 함수 반환 타입 추출
function getUser() {
return { id: 1, name: 'Alice' }
}
type UserReturn = ReturnType<typeof getUser> // { id: number; name: string }
// Parameters<T> - 함수 매개변수 타입 추출 (튜플)
function createUser(name: string, age: number, active: boolean) {}
type CreateUserParams = Parameters<typeof createUser> // [string, number, boolean]
// ConstructorParameters<T> - 생성자 매개변수 타입 추출
class ApiClient {
constructor(
public baseUrl: string,
public timeout: number
) {}
}
type ClientParams = ConstructorParameters<typeof ApiClient> // [string, number]
2.3 Mapped Types
// 기본 Mapped Type
type Flags<T> = {
[K in keyof T]: boolean
}
interface Config {
darkMode: string
notifications: string
analytics: string
}
type ConfigFlags = Flags<Config>
// { darkMode: boolean; notifications: boolean; analytics: boolean }
// 수정자 추가/제거
type Mutable<T> = {
-readonly [K in keyof T]: T[K] // readonly 제거
}
type AllRequired<T> = {
[K in keyof T]-?: T[K] // ? 선택성 제거
}
// 키 재매핑 (TypeScript 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }
// 필터링을 위한 키 재매핑
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
2.4 Conditional Types
// 기본 조건부 타입
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// Distributive Conditional Types (Union에 분배)
type ToArray<T> = T extends any ? T[] : never
type C = ToArray<string | number> // string[] | number[]
// infer 키워드 - 조건부 타입 안에서 타입 추론
type UnpackArray<T> = T extends Array<infer Item> ? Item : T
type D = UnpackArray<string[]> // string
type E = UnpackArray<number> // number (배열이 아니므로 그대로)
type UnpackPromise<T> = T extends Promise<infer R> ? R : T
type F = UnpackPromise<Promise<string>> // string
// 함수 반환 타입 직접 구현 (ReturnType의 원리)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type G = MyReturnType<() => string> // string
// 깊은 타입 조작
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
3. 제네릭 (Generics)
3.1 제네릭 함수
// 기본 제네릭 함수
function identity<T>(value: T): T {
return value
}
const str = identity<string>('hello') // string
const num = identity(42) // number (타입 추론)
// 여러 타입 파라미터
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second]
}
// 배열의 첫 번째 요소 반환
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
// 제네릭 화살표 함수 (TSX 파일에서는 주의 필요)
const wrap = <T>(value: T): { value: T } => ({ value })
3.2 제네릭 인터페이스와 클래스
// 제네릭 인터페이스
interface Repository<T> {
findById(id: number): Promise<T | null>
findAll(): Promise<T[]>
create(item: Omit<T, 'id'>): Promise<T>
update(id: number, item: Partial<T>): Promise<T>
delete(id: number): Promise<void>
}
// 제네릭 클래스
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
get size(): number {
return this.items.length
}
isEmpty(): boolean {
return this.items.length === 0
}
}
const stack = new Stack<number>()
stack.push(1)
stack.push(2)
console.log(stack.pop()) // 2
3.3 제네릭 제약 (extends)
// extends로 타입 제약
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Alice', age: 30 }
const name = getProperty(user, 'name') // string
// getProperty(user, 'email') // Error: 'email'은 keyof typeof user가 아님
// 인터페이스 제약
interface HasId {
id: number
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id)
}
// 다중 제약
interface Serializable {
serialize(): string
}
function processAndSerialize<T extends HasId & Serializable>(item: T): string {
return `${item.id}: ${item.serialize()}`
}
3.4 실용적인 제네릭 패턴
// API Response 타입 패턴
interface ApiResponse<T> {
data: T
status: number
message: string
timestamp: string
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const response = await fetch('/api/users')
return response.json()
}
// Repository 패턴
class UserRepository implements Repository<User> {
private baseUrl = '/api/users'
async findById(id: number): Promise<User | null> {
const res = await fetch(`${this.baseUrl}/${id}`)
if (!res.ok) return null
return res.json()
}
async findAll(): Promise<User[]> {
const res = await fetch(this.baseUrl)
return res.json()
}
async create(item: Omit<User, 'id'>): Promise<User> {
const res = await fetch(this.baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
})
return res.json()
}
async update(id: number, item: Partial<User>): Promise<User> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
})
return res.json()
}
async delete(id: number): Promise<void> {
await fetch(`${this.baseUrl}/${id}`, { method: 'DELETE' })
}
}
4. 고급 타입 기법
4.1 Union Types와 Intersection Types
// Union Types
type StringOrNumber = string | number
type Nullable<T> = T | null
type Optional<T> = T | null | undefined
// Intersection Types
interface Named {
name: string
}
interface Aged {
age: number
}
type Person = Named & Aged
const person: Person = { name: 'Alice', age: 30 }
// 유용한 intersection 패턴
type WithTimestamp<T> = T & {
createdAt: Date
updatedAt: Date
}
type UserWithTimestamp = WithTimestamp<User>
4.2 Discriminated Unions (Tagged Union)
// 각 케이스에 고유한 리터럴 타입 판별자(discriminant) 사용
interface LoadingState {
status: 'loading'
}
interface SuccessState<T> {
status: 'success'
data: T
}
interface ErrorState {
status: 'error'
error: Error
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState
// 판별자를 이용한 타입 좁히기
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return '로딩 중...'
case 'success':
return `데이터: ${JSON.stringify(state.data)}`
case 'error':
return `오류: ${state.error.message}`
// default: TypeScript가 모든 케이스를 처리했음을 알고 있음
}
}
// Redux 액션 패턴
type Action =
| { type: 'INCREMENT'; payload: number }
| { type: 'DECREMENT'; payload: number }
| { type: 'RESET' }
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
case 'RESET':
return 0
}
}
4.3 Type Guards
// typeof 타입 가드
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase() // value: string
}
return value.toFixed(2) // value: number
}
// instanceof 타입 가드
class Cat {
meow() {
return '야옹'
}
}
class Dog {
bark() {
return '멍멍'
}
}
function speak(animal: Cat | Dog): string {
if (animal instanceof Cat) {
return animal.meow() // animal: Cat
}
return animal.bark() // animal: Dog
}
// in 연산자 타입 가드
interface Fish {
swim(): void
}
interface Bird {
fly(): void
}
function move(animal: Fish | Bird): void {
if ('swim' in animal) {
animal.swim() // animal: Fish
} else {
animal.fly() // animal: Bird
}
}
// 사용자 정의 타입 가드 (is 키워드)
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj
}
// Assertion Function (TypeScript 3.7+)
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`)
}
}
const maybeString: unknown = 'hello'
assertIsString(maybeString)
console.log(maybeString.toUpperCase()) // 이후 string으로 확정
4.4 Overload Signatures
// 오버로드 시그니처
function process(x: number): number
function process(x: string): string
function process(x: number[]): number[]
function process(x: number | string | number[]): number | string | number[] {
if (typeof x === 'number') return x * 2
if (typeof x === 'string') return x.toUpperCase()
return x.map((n) => n * 2)
}
const r1 = process(10) // number
const r2 = process('hello') // string
const r3 = process([1, 2, 3]) // number[]
// 메서드 오버로드
class EventEmitter {
on(event: 'data', listener: (data: Buffer) => void): this
on(event: 'end', listener: () => void): this
on(event: 'error', listener: (err: Error) => void): this
on(event: string, listener: (...args: any[]) => void): this {
// 구현
return this
}
}
5. 클래스와 데코레이터
5.1 접근 제한자와 클래스 기능
class BankAccount {
readonly accountNumber: string
private _balance: number
protected owner: string
constructor(accountNumber: string, owner: string, initialBalance = 0) {
this.accountNumber = accountNumber
this.owner = owner
this._balance = initialBalance
}
// getter/setter
get balance(): number {
return this._balance
}
// private 메서드
private validateAmount(amount: number): void {
if (amount <= 0) throw new Error('금액은 0보다 커야 합니다')
}
deposit(amount: number): void {
this.validateAmount(amount)
this._balance += amount
}
withdraw(amount: number): void {
this.validateAmount(amount)
if (amount > this._balance) throw new Error('잔액 부족')
this._balance -= amount
}
// 정적 메서드
static createSavingsAccount(owner: string): BankAccount {
const accountNumber = `SAV-${Date.now()}`
return new BankAccount(accountNumber, owner, 0)
}
}
// 생성자 매개변수 단축 (Parameter Properties)
class Point {
constructor(
public readonly x: number,
public readonly y: number
) {}
distance(other: Point): number {
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2)
}
}
5.2 Abstract Class
abstract class Shape {
abstract area(): number
abstract perimeter(): number
// 공통 구현
toString(): string {
return `넓이: ${this.area().toFixed(2)}, 둘레: ${this.perimeter().toFixed(2)}`
}
}
class Circle extends Shape {
constructor(private radius: number) {
super()
}
area(): number {
return Math.PI * this.radius ** 2
}
perimeter(): number {
return 2 * Math.PI * this.radius
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super()
}
area(): number {
return this.width * this.height
}
perimeter(): number {
return 2 * (this.width + this.height)
}
}
5.3 Decorators
데코레이터는 tsconfig.json에서 experimentalDecorators: true를 설정해야 합니다.
// 클래스 데코레이터
function Singleton<T extends new (...args: any[]) => {}>(constructor: T) {
let instance: InstanceType<T>
return class extends constructor {
constructor(...args: any[]) {
if (instance) return instance
super(...args)
instance = this as any
}
}
}
@Singleton
class AppConfig {
readonly version = '1.0.0'
}
// 메서드 데코레이터
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`호출: ${propertyKey}(${JSON.stringify(args)})`)
const result = original.apply(this, args)
console.log(`반환: ${JSON.stringify(result)}`)
return result
}
return descriptor
}
// 속성 데코레이터
function Validate(min: number, max: number) {
return function (target: any, propertyKey: string) {
let value: number
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue: number) => {
if (newValue < min || newValue > max) {
throw new Error(`${propertyKey}은 ${min}-${max} 범위여야 합니다`)
}
value = newValue
},
})
}
}
class Product {
name: string
@Validate(0, 1000000)
price: number
constructor(name: string, price: number) {
this.name = name
this.price = price
}
@Log
getInfo(): string {
return `${this.name}: ${this.price}원`
}
}
6. 모듈 시스템과 Declaration Files
6.1 ES Modules in TypeScript
// named export
export interface User {
id: number
name: string
}
export function createUser(name: string): User {
return { id: Date.now(), name }
}
// default export
export default class UserService {
private users: User[] = []
add(user: User): void {
this.users.push(user)
}
}
// re-export
export { User as UserType } from './types'
export * from './utils'
export * as helpers from './helpers'
// import
import UserService, { User, createUser } from './user'
import type { User as UserType } from './user' // 타입만 import (런타임 제거)
6.2 Declaration Files (.d.ts)
// globals.d.ts - 전역 타입 선언
declare global {
interface Window {
analytics: {
track(event: string, properties?: Record<string, unknown>): void
}
}
}
// 모듈 선언 (타입 없는 JS 라이브러리 보완)
declare module 'legacy-lib' {
export function doSomething(value: string): number
export const VERSION: string
}
// 파일 타입 선언 (예: SVG 파일)
declare module '*.svg' {
const content: string
export default content
}
declare module '*.png' {
const content: string
export default content
}
6.3 tsconfig.json 핵심 옵션
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"]
},
"baseUrl": ".",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true
}
}
7. 실용 패턴
7.1 Builder Pattern with Method Chaining
class QueryBuilder<T> {
private conditions: string[] = []
private orderByClause?: string
private limitValue?: number
private offsetValue?: number
where(condition: string): this {
this.conditions.push(condition)
return this
}
orderBy(field: keyof T & string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderByClause = `${field} ${direction}`
return this
}
limit(n: number): this {
this.limitValue = n
return this
}
offset(n: number): this {
this.offsetValue = n
return this
}
build(): string {
let query = 'SELECT * FROM table'
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`
}
if (this.orderByClause) {
query += ` ORDER BY ${this.orderByClause}`
}
if (this.limitValue !== undefined) {
query += ` LIMIT ${this.limitValue}`
}
if (this.offsetValue !== undefined) {
query += ` OFFSET ${this.offsetValue}`
}
return query
}
}
// 사용 예시
const query = new QueryBuilder<User>()
.where('age > 18')
.where('active = true')
.orderBy('name')
.limit(10)
.offset(0)
.build()
7.2 Result Type (Either Monad 패턴)
type Ok<T> = { success: true; data: T }
type Err<E> = { success: false; error: E }
type Result<T, E = Error> = Ok<T> | Err<E>
function ok<T>(data: T): Ok<T> {
return { success: true, data }
}
function err<E>(error: E): Err<E> {
return { success: false, error }
}
// 사용 예시
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
return err(new Error(`HTTP ${response.status}`))
}
const data = await response.json()
return ok(data)
} catch (e) {
return err(e instanceof Error ? e : new Error('Unknown error'))
}
}
// 호출부에서 명시적 에러 처리 강제
const result = await fetchUser(1)
if (result.success) {
console.log(result.data.name)
} else {
console.error(result.error.message)
}
7.3 Branded Types (타입 안전한 ID)
// 브랜드 타입 - 같은 기반 타입이지만 서로 호환 불가
type UserId = number & { readonly _brand: 'UserId' }
type PostId = number & { readonly _brand: 'PostId' }
type CommentId = number & { readonly _brand: 'CommentId' }
// 생성 함수 (as unknown as BrandedType 패턴)
function asUserId(id: number): UserId {
return id as UserId
}
function asPostId(id: number): PostId {
return id as PostId
}
function getUser(id: UserId): User {
/* ... */ return {} as User
}
function getPost(id: PostId): void {
/* ... */
}
const userId = asUserId(1)
const postId = asPostId(1)
getUser(userId) // OK
// getUser(postId) // Error: PostId는 UserId에 할당 불가
// getUser(1) // Error: number는 UserId에 할당 불가
7.4 satisfies 연산자 (TypeScript 4.9+)
// satisfies - 타입을 검증하되 추론된 타입은 유지
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>
// satisfies 덕분에:
palette.red.map((x) => x * 2) // OK: number[]로 추론됨
palette.green.toUpperCase() // OK: string으로 추론됨
// 타입 단언(as) 없이도 올바른 타입 유지
const config = {
port: 3000,
host: 'localhost',
features: {
auth: true,
logging: false,
},
} satisfies {
port: number
host: string
features: Record<string, boolean>
}
8. 퀴즈
퀴즈 1: unknown vs any
다음 코드에서 컴파일 오류가 발생하는 줄을 고르시오.
const a: any = 'hello'
const b: unknown = 'world'
console.log(a.toUpperCase()) // (1)
console.log(b.toUpperCase()) // (2)
if (typeof b === 'string') {
console.log(b.toUpperCase()) // (3)
}
정답: (2)번 줄
설명: unknown 타입은 타입 가드 없이 속성/메서드에 직접 접근할 수 없습니다. any는 타입 검사를 완전히 비활성화하므로 (1)은 통과되지만, unknown은 타입 좁히기 후에만 사용할 수 있어 (2)는 컴파일 오류입니다. (3)은 typeof 타입 가드 후이므로 OK입니다.
퀴즈 2: 유틸리티 타입 조합
다음 타입을 유틸리티 타입으로 표현하시오.
interface Article {
id: number
title: string
content: string
author: string
publishedAt: Date
updatedAt: Date
}
// 목표: id, publishedAt, updatedAt을 제외하고 모든 필드를 선택적으로 만든 타입
type ArticleUpdateInput = /* ??? */
정답: Partial<Omit<Article, 'id' | 'publishedAt' | 'updatedAt'>>
설명: 먼저 Omit으로 수정 불가 필드를 제거하고, Partial로 나머지 모든 필드를 선택적으로 만듭니다. 유틸리티 타입은 이처럼 조합하여 사용할 수 있습니다.
퀴즈 3: 제네릭 제약
다음 함수가 컴파일 오류 없이 동작하도록 제네릭 제약을 추가하시오.
function mergeObjects<T, U>(target: T, source: U): T & U {
return { ...target, ...source }
}
// 요구사항: T와 U 모두 object 타입이어야 함
정답: function mergeObjects<T extends object, U extends object>(target: T, source: U): T & U
설명: extends object를 사용하면 기본 타입(string, number 등)의 전달을 방지하고 객체 타입만 허용합니다. object 대신 Record<string, unknown>을 쓰는 것이 더 명시적인 경우도 있습니다.
퀴즈 4: Discriminated Union 완전성 검사
아래 코드에서 새로운 도형 Triangle을 추가했을 때 컴파일 오류가 발생하지 않는 이유와, 오류를 발생시키려면 어떻게 수정해야 하는지 설명하시오.
type Shape = Circle | Square // Triangle 추가 후: Circle | Square | Triangle
function getArea(shape: Shape): number {
if (shape.kind === 'circle') return Math.PI * shape.radius ** 2
if (shape.kind === 'square') return shape.side ** 2
return 0 // 이 줄로 인해 컴파일 오류 없음
}
정답: return 0 대신 const _: never = shape; throw new Error(...) 패턴 사용
설명: return 0은 모든 미처리 케이스를 조용히 처리합니다. never 완전성 검사 패턴을 사용하면 Triangle이 추가될 때 shape가 never가 아니므로 컴파일 오류가 발생합니다.
퀴즈 5: infer와 조건부 타입
다음 타입의 결과를 예측하시오.
type Flatten<T> = T extends Array<infer Item> ? Item : T
type A = Flatten<string[]> // ?
type B = Flatten<number[][]> // ?
type C = Flatten<string> // ?
type D = Flatten<string | number[]> // ?
정답: A = string, B = number[], C = string, D = string | number
설명: Flatten은 배열의 한 단계만 풀어냅니다. B는 number[][]를 Array<number[]>로 보기 때문에 number[]가 됩니다. D는 Union에 분배되어 Flatten<string> | Flatten<number[]> = string | number가 됩니다.
마치며
TypeScript의 타입 시스템은 단순한 문법 설탕이 아닙니다. 올바르게 사용하면 런타임 오류를 컴파일 타임에 잡아내고, IDE의 자동완성과 리팩토링 지원을 최대한 활용할 수 있습니다.
핵심 정리:
unknown을any대신 활용하여 타입 안전성 유지- 유틸리티 타입과 Mapped Type으로 DRY한 타입 정의
- Discriminated Union으로 상태 머신을 안전하게 모델링
- 제네릭으로 재사용 가능한 타입 안전 코드 작성
satisfies연산자로 타입 검증과 추론을 동시에