Skip to content

필사 모드: TypeScript 완전 정복: 타입 시스템부터 고급 패턴까지 실전 가이드

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

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. 퀴즈

다음 코드에서 컴파일 오류가 발생하는 줄을 고르시오.

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입니다.

다음 타입을 유틸리티 타입으로 표현하시오.

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`로 나머지 모든 필드를 선택적으로 만듭니다. 유틸리티 타입은 이처럼 조합하여 사용할 수 있습니다.

다음 함수가 컴파일 오류 없이 동작하도록 제네릭 제약을 추가하시오.

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>`을 쓰는 것이 더 명시적인 경우도 있습니다.

아래 코드에서 새로운 도형 `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`가 아니므로 컴파일 오류가 발생합니다.

다음 타입의 결과를 예측하시오.

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` 연산자로 타입 검증과 추론을 동시에

현재 단락 (1/869)

TypeScript는 JavaScript에 정적 타입 시스템을 추가한 언어입니다. 2026년 현재 프론트엔드와 백엔드를 막론하고 대부분의 대규모 프로젝트에서 TypeScript를 ...

작성 글자: 0원문 글자: 20,738작성 단락: 0/869