Split View: TypeScript 완전 정복: 타입 시스템부터 고급 패턴까지 실전 가이드
TypeScript 완전 정복: 타입 시스템부터 고급 패턴까지 실전 가이드
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연산자로 타입 검증과 추론을 동시에
TypeScript Complete Guide: Type System to Advanced Patterns
TypeScript Complete Guide: Type System to Advanced Patterns
TypeScript adds a static type system on top of JavaScript. As of 2026, most large-scale projects — both frontend and backend — have adopted TypeScript as their standard. This guide covers everything from the basics to advanced patterns that you can apply directly in production code.
1. TypeScript Type System Fundamentals
1.1 Primitive Types
TypeScript's primitive types mirror JavaScript's but are declared explicitly.
// Basic primitive types
const name: string = 'Alice'
const age: number = 30
const isActive: boolean = true
const nothing: null = null
const undef: undefined = undefined
// ES6+ types
const sym: symbol = Symbol('unique')
const big: bigint = 9007199254740991n
// Type inference - TypeScript infers the type from the assigned value
const inferredName = 'Alice' // inferred as string
const inferredAge = 30 // inferred as number
1.2 Arrays and Tuples
// Two equivalent array type notations
const nums1: number[] = [1, 2, 3]
const nums2: Array<number> = [1, 2, 3]
// Read-only array
const readonlyNums: readonly number[] = [1, 2, 3]
// readonlyNums.push(4) // Error: push does not exist on readonly array
// Tuple - fixed length and type at each position
const pair: [string, number] = ['Alice', 30]
const triple: [string, number, boolean] = ['Bob', 25, true]
// Optional tuple element
const optional: [string, number?] = ['Charlie']
// Rest tuple
const rest: [string, ...number[]] = ['start', 1, 2, 3, 4]
1.3 Enums
// Numeric enum (default: starts from 0)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// String enum
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE',
}
// const enum - inlined at compile time, reducing bundle size
const enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING',
}
// When using const enum, the value is substituted directly
const currentStatus = Status.Active // compiled to: 'ACTIVE'
// When to choose regular enum vs const enum:
// - Need to iterate the enum object at runtime: use regular enum
// - Performance matters and iteration is not needed: use const enum
1.4 any vs unknown vs never vs void
These four special types are the most commonly confused in TypeScript.
// any - completely disables type checking (avoid when possible)
let anyVal: any = 'string'
anyVal = 42 // OK
anyVal = true // OK
anyVal.nonExistent() // possible runtime error, no compile error
// unknown - safer alternative to any
let unknownVal: unknown = 'string'
// unknownVal.toUpperCase() // Error: needs type narrowing first
if (typeof unknownVal === 'string') {
unknownVal.toUpperCase() // OK: after type guard
}
// never - type for values that never occur
function throwError(msg: string): never {
throw new Error(msg)
}
function infiniteLoop(): never {
while (true) {}
}
// never is useful for exhaustiveness checks
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 must be never here - adding a new Shape causes a compile error
const _exhaustive: never = shape
throw new Error(`Unknown shape: ${_exhaustive}`)
}
}
// void - for functions with no return value
function logMessage(msg: string): void {
console.log(msg)
}
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
}
// Key differences:
// 1. Interface supports Declaration Merging
interface IPoint {
z?: number // adds z to existing IPoint
}
// 2. Type Alias supports union, intersection, and primitive types
type StringOrNumber = string | number
type WithTimestamp = Point & { createdAt: Date }
// 3. Both can be implemented by classes
class MyPoint implements IPoint {
x = 0
y = 0
}
// When to choose:
// - Public APIs for libraries/SDKs: Interface (extensible via declaration merging)
// - Internal app types: Type Alias (more flexible)
// - Object shapes only: either works fine
1.6 Literal Types and Template Literal Types
// 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
// Template literal types (TypeScript 4.1+)
type EventName = 'click' | 'focus' | 'blur'
type Handler = `on${Capitalize<EventName>}`
// Result: 'onClick' | 'onFocus' | 'onBlur'
type CSSUnit = 'px' | 'em' | 'rem' | 'vw' | 'vh'
type CSSValue = `${number}${CSSUnit}`
// Examples: '16px', '1.5em', '100vw', etc.
// Practical example: API route types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
type APIRoute = `${HttpMethod} ${Endpoint}`
// Result: 12 combinations like 'GET /users', 'POST /posts', etc.
2. Object Types and Utility Types
2.1 Advanced Object Type Features
interface User {
readonly id: number // read-only
name: string
email?: string // optional property
[key: string]: unknown // index signature
}
const user: User = { id: 1, name: 'Alice' }
// user.id = 2 // Error: readonly
2.2 Built-in Utility Types
interface Todo {
id: number
title: string
completed: boolean
description: string
}
// Partial<T> - makes all properties optional
type PartialTodo = Partial<Todo>
// Required<T> - makes all properties required
type RequiredTodo = Required<Partial<Todo>>
// Readonly<T> - makes all properties read-only
type ReadonlyTodo = Readonly<Todo>
// Pick<T, K> - picks specific properties
type TodoPreview = Pick<Todo, 'id' | 'title'>
// Omit<T, K> - omits specific properties
type TodoWithoutDescription = Omit<Todo, 'description'>
// Record<K, V> - creates a key-value map type
type TodoMap = Record<number, Todo>
// Exclude<T, U> - removes U from the union T
type T1 = Exclude<string | number | boolean, boolean> // string | number
// Extract<T, U> - keeps only U from the union T
type T2 = Extract<string | number | boolean, string | boolean> // string | boolean
// NonNullable<T> - removes null and undefined
type T3 = NonNullable<string | null | undefined> // string
// ReturnType<T> - extracts the return type of a function
function getUser() {
return { id: 1, name: 'Alice' }
}
type UserReturn = ReturnType<typeof getUser> // { id: number; name: string }
// Parameters<T> - extracts parameter types as a tuple
function createUser(name: string, age: number, active: boolean) {}
type CreateUserParams = Parameters<typeof createUser> // [string, number, boolean]
// ConstructorParameters<T> - extracts constructor parameter types
class ApiClient {
constructor(
public baseUrl: string,
public timeout: number
) {}
}
type ClientParams = ConstructorParameters<typeof ApiClient> // [string, number]
2.3 Mapped Types
// Basic 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 }
// Adding/removing modifiers
type Mutable<T> = {
-readonly [K in keyof T]: T[K] // removes readonly
}
type AllRequired<T> = {
[K in keyof T]-?: T[K] // removes optionality
}
// Key remapping (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 }
// Key remapping for filtering
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
2.4 Conditional Types
// Basic conditional type
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// Distributive conditional types (distributed over unions)
type ToArray<T> = T extends any ? T[] : never
type C = ToArray<string | number> // string[] | number[]
// The infer keyword - infers a type within a conditional type
type UnpackArray<T> = T extends Array<infer Item> ? Item : T
type D = UnpackArray<string[]> // string
type E = UnpackArray<number> // number (not an array, returned as-is)
type UnpackPromise<T> = T extends Promise<infer R> ? R : T
type F = UnpackPromise<Promise<string>> // string
// Implementing ReturnType manually
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type G = MyReturnType<() => string> // string
// Deep type manipulation
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
3. Generics
3.1 Generic Functions
// Basic generic function
function identity<T>(value: T): T {
return value
}
const str = identity<string>('hello') // string
const num = identity(42) // number (inferred)
// Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second]
}
// Return the first element of an array
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
3.2 Generic Interfaces and Classes
// Generic interface
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>
}
// Generic class
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 Generic Constraints (extends)
// Constraining with 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' is not keyof typeof user
// Interface constraint
interface HasId {
id: number
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id)
}
// Multiple constraints
interface Serializable {
serialize(): string
}
function processAndSerialize<T extends HasId & Serializable>(item: T): string {
return `${item.id}: ${item.serialize()}`
}
3.4 Practical Generic Patterns
// API Response type pattern
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
}
}
// Repository pattern implementation
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. Advanced Type Techniques
4.1 Union Types and 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 }
// Useful intersection pattern
type WithTimestamp<T> = T & {
createdAt: Date
updatedAt: Date
}
4.2 Discriminated Unions (Tagged Union / Algebraic Data Types)
// Each case has a unique literal type 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
// Type narrowing via discriminant
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...'
case 'success':
return `Data: ${JSON.stringify(state.data)}`
case 'error':
return `Error: ${state.error.message}`
}
}
// Redux action pattern
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 type guard
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase() // value: string
}
return value.toFixed(2) // value: number
}
// instanceof type guard
class Cat {
meow() {
return 'Meow'
}
}
class Dog {
bark() {
return 'Woof'
}
}
function speak(animal: Cat | Dog): string {
if (animal instanceof Cat) {
return animal.meow() // animal: Cat
}
return animal.bark() // animal: Dog
}
// in operator type guard
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
}
}
// Custom type guard (is keyword)
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()) // narrowed to string after assertion
4.4 Overload Signatures
// Function overloads
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[]
// Method overloads
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 {
// implementation
return this
}
}
5. Classes and Decorators
5.1 Access Modifiers and Class Features
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
}
get balance(): number {
return this._balance
}
private validateAmount(amount: number): void {
if (amount <= 0) throw new Error('Amount must be greater than 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('Insufficient funds')
this._balance -= amount
}
static createSavingsAccount(owner: string): BankAccount {
const accountNumber = `SAV-${Date.now()}`
return new BankAccount(accountNumber, owner, 0)
}
}
// Constructor parameter shorthand (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 Classes
abstract class Shape {
abstract area(): number
abstract perimeter(): number
// Shared implementation
toString(): string {
return `Area: ${this.area().toFixed(2)}, Perimeter: ${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
Decorators require experimentalDecorators: true in tsconfig.json.
// Class decorator
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'
}
// Method decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`Calling: ${propertyKey}(${JSON.stringify(args)})`)
const result = original.apply(this, args)
console.log(`Returned: ${JSON.stringify(result)}`)
return result
}
return descriptor
}
// Property decorator
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} must be between ${min} and ${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. Module System and Declaration Files
6.1 ES Modules in TypeScript
// Named exports
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-exports
export { User as UserType } from './types'
export * from './utils'
export * as helpers from './helpers'
// Imports
import UserService, { User, createUser } from './user'
import type { User as UserType } from './user' // type-only import (removed at runtime)
6.2 Declaration Files (.d.ts)
// globals.d.ts - global type declarations
declare global {
interface Window {
analytics: {
track(event: string, properties?: Record<string, unknown>): void
}
}
}
// Module declaration (augmenting untyped JS libraries)
declare module 'legacy-lib' {
export function doSomething(value: string): number
export const VERSION: string
}
// File type declarations (e.g., SVG files)
declare module '*.svg' {
const content: string
export default content
}
declare module '*.png' {
const content: string
export default content
}
6.3 Key tsconfig.json Options
{
"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. Practical Patterns
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
}
}
// Usage example
const query = new QueryBuilder<User>()
.where('age > 18')
.where('active = true')
.orderBy('name')
.limit(10)
.offset(0)
.build()
7.2 Result Type (Either Monad Pattern)
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 }
}
// Usage example
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'))
}
}
// Caller is forced to handle errors explicitly
const result = await fetchUser(1)
if (result.success) {
console.log(result.data.name)
} else {
console.error(result.error.message)
}
7.3 Branded Types (Type-Safe IDs)
// Branded types - same underlying type but mutually incompatible
type UserId = number & { readonly _brand: 'UserId' }
type PostId = number & { readonly _brand: 'PostId' }
type CommentId = number & { readonly _brand: 'CommentId' }
// Factory functions
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 is not assignable to UserId
// getUser(1) // Error: number is not assignable to UserId
7.4 The satisfies Operator (TypeScript 4.9+)
// satisfies - validates the type while preserving the inferred type
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>
// Because of satisfies:
palette.red.map((x) => x * 2) // OK: inferred as number[]
palette.green.toUpperCase() // OK: inferred as string
// No type assertion needed while still enforcing the contract
const config = {
port: 3000,
host: 'localhost',
features: {
auth: true,
logging: false,
},
} satisfies {
port: number
host: string
features: Record<string, boolean>
}
8. Quiz
Quiz 1: unknown vs any
Identify which line causes a compile error in the following code.
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)
}
Answer: Line (2)
Explanation: The unknown type cannot be accessed without a type guard. any disables type checking entirely, so (1) compiles fine. unknown requires narrowing before use, so (2) is a compile error. (3) is fine because the typeof guard has already narrowed the type.
Quiz 2: Composing Utility Types
Express the following type using utility types.
interface Article {
id: number
title: string
content: string
author: string
publishedAt: Date
updatedAt: Date
}
// Goal: make all fields optional except id, publishedAt, and updatedAt
type ArticleUpdateInput = /* ??? */
Answer: Partial<Omit<Article, 'id' | 'publishedAt' | 'updatedAt'>>
Explanation: First use Omit to remove the non-editable fields, then use Partial to make all remaining fields optional. Utility types can be composed this way.
Quiz 3: Generic Constraints
Add generic constraints so the following function compiles without errors.
function mergeObjects<T, U>(target: T, source: U): T & U {
return { ...target, ...source }
}
// Requirement: both T and U must be object types
Answer: function mergeObjects<T extends object, U extends object>(target: T, source: U): T & U
Explanation: Using extends object prevents passing primitive types (string, number, etc.) and only allows object types. Using Record<string, unknown> is sometimes more explicit.
Quiz 4: Discriminated Union Exhaustiveness
Explain why adding a new shape Triangle to the union does not cause a compile error below, and how to fix it so it does.
type Shape = Circle | Square // After adding 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 // This prevents the compile error
}
Answer: Replace return 0 with the never exhaustiveness check pattern: const _: never = shape; throw new Error(...)
Explanation: return 0 silently handles all unmatched cases. Using the never exhaustiveness check makes the compiler complain when Triangle is added, because shape would no longer be never at that point.
Quiz 5: infer and Conditional Types
Predict the result of the following types.
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[]> // ?
Answer: A = string, B = number[], C = string, D = string | number
Explanation: Flatten unwraps only one level of array. B sees number[][] as Array<number[]> and returns number[]. D distributes over the union: Flatten<string> | Flatten<number[]> = string | number.
Summary
TypeScript's type system is not mere syntax sugar. When used correctly, it catches runtime errors at compile time and unlocks full IDE autocompletion and refactoring support.
Key takeaways:
- Prefer
unknownoveranyto maintain type safety - Use utility types and mapped types for DRY type definitions
- Model state machines safely with discriminated unions
- Write reusable, type-safe code with generics
- Use the
satisfiesoperator to validate and infer types simultaneously