Backend
home
📘

TypeScript 기본 문법 완전 정복 — 타입 시스템부터 실무 패턴까지

생성일
2026/04/29 12:15
태그
TypeScript

1. TypeScript란?

TypeScript는 Microsoft가 개발한 JavaScript의 상위 집합(Superset). 모든 JavaScript 코드는 유효한 TypeScript 코드이며, TypeScript는 컴파일 시 JavaScript로 변환됨.

JavaScript vs TypeScript

// JavaScript — 런타임에 터져야 알스 다음 function add(a, b) { return a + b; } add(1, '2'); // '12' — 의도치 않은 문자열 결합 // TypeScript — 컴파일 시점에 오류 감지 function add(a: number, b: number): number { return a + b; } add(1, '2'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
JavaScript
복사

TypeScript를 써야 하는 이유

컴파일 시점 오류 발견 — 런타임 버그를 사전에 막음
IDE 자동완성 / 코드 힌트 — 개발 생산성 대폭 향상
코드 가독성 — 타입이 문서 역할
대규모 프로젝트 유지보수 — 리팩토링이 안전함

2. 기본 타입

Primitive Types

// 기본 원시 타입 let name: string = 'codesche'; let age: number = 30; let isActive: boolean = true; let nothing: null = null; let undef: undefined = undefined; // 타입 추론 — 다음과 같이 써도 TS가 자동 추론 let name = 'codesche'; // string let age = 30; // number let isActive = true; // boolean
TypeScript
복사

any, unknown, never, void

// any: 타입 검사 포기 — 가능하면 사용 지양 let anything: any = 42; anything = 'hello'; // OK anything.foo.bar.baz; // OK (컴파일 통과, 런타임 실패 가능) // unknown: any의 안전한 대안 — 사용 전 타입 좌소(narrowing) 필수 let value: unknown = fetchSomething(); if (typeof value === 'string') { console.log(value.toUpperCase()); // narrowing 후 안전 } // void: 반환값이 없는 함수 function logMessage(msg: string): void { console.log(msg); } // never: 절대 도달할 수 없는 타입 (다 커버된 switch 등) function throwError(msg: string): never { throw new Error(msg); }
TypeScript
복사

3. 배열과 튜플

// 배열 타입 const nums: number[] = [1, 2, 3]; const strs: Array<string> = ['a', 'b', 'c']; // 다차원 배열 const matrix: number[][] = [[1, 2], [3, 4]]; // 튜플 (Tuple): 길이와 타입이 고정된 배열 const point: [number, number] = [10, 20]; const entry: [string, number] = ['age', 30]; // 네이밍된 튜플 (TS 4.0+) const rgb: [r: number, g: number, b: number] = [255, 128, 0]; // 튜플 실무 예: useState 반환 타입 표현 function useState<T>(initial: T): [T, (val: T) => void] { let state = initial; const setState = (val: T) => { state = val; }; return [state, setState]; } const [count, setCount] = useState(0); // [number, (val: number) => void]
TypeScript
복사

4. 인터페이스(Interface)와 타입 별칭(Type Alias)

Interface

interface User { id: number; name: string; email: string; role?: 'admin' | 'user'; // 선택적 프로퍼티 readonly createdAt: Date; // 읽기 전용 } // 확장 (extends) interface Admin extends User { permissions: string[]; } // 선언 합치 — 같은 이름으로 여러 번 선언 가능 interface User { phone?: string; }
TypeScript
복사

Type Alias

type Point = { x: number; y: number; }; // 유니온, 리터럴 타입에 유리 type ID = string | number; type Status = 'pending' | 'active' | 'inactive'; type Nullable<T> = T | null; // 함수 타입 type Callback = (err: Error | null, result: string) => void;
TypeScript
복사

Interface vs Type Alias 비교

구분
Interface
Type Alias
확장
extends
& (인터섹션)
합치
가능 (Declaration Merging)
불가
유니온 타입
불가
가능
권장 상황
객체/클래스 형태
복합타입, 유틸리티 타입

5. 유니온(Union)과 인터섹션(Intersection)

// Union: A 또는 B type StringOrNumber = string | number; type Result = 'success' | 'error' | 'loading'; function formatId(id: string | number): string { return id.toString(); } // Intersection: A 그리고 B 모두 type Timestamped = { createdAt: Date; updatedAt: Date; }; type Named = { name: string; }; type NamedTimestamped = Named & Timestamped; // Discriminated Union — 실무에서 매우 유용 type Shape = | { kind: 'circle'; radius: number } | { kind: 'rectangle'; width: number; height: number } | { kind: 'triangle'; base: number; height: number }; function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'rectangle': return shape.width * shape.height; case 'triangle': return (shape.base * shape.height) / 2; } }
TypeScript
복사

6. 열거형(Enum)

// 숫자형 Enum enum Direction { Up, // 0 Down, // 1 Left, // 2 Right, // 3 } // 문자열 Enum — 실무에서 더 자주 씀 enum Status { Pending = 'PENDING', Active = 'ACTIVE', Inactive = 'INACTIVE', } function handleStatus(status: Status) { if (status === Status.Active) { console.log('활성 상태'); } } // const enum: 컴파일 시 인라인 대체 — 번들 파일 크기 최소화 const enum HttpMethod { GET = 'GET', POST = 'POST', PUT = 'PUT', DELETE = 'DELETE', }
TypeScript
복사

7. 제네릭(Generics)

타입을 매개변수처럼 다루는 기법. 다양한 타입에 재사용 가능한 코드를 작성할 수 있음.

기본 제네릭

function identity<T>(value: T): T { return value; } identity<string>('hello'); // 명시 identity(42); // 추론: number로 자동 추론 // 제네릭 인터페이스 interface ApiResponse<T> { data: T; status: number; message: string; } type UserResponse = ApiResponse<User>; type OrderResponse = ApiResponse<Order[]>;
TypeScript
복사

제네릭 제약 (Constraints)

// T는 반드시 length 프로퍼티를 가져야 함 function getLength<T extends { length: number }>(value: T): number { return value.length; } getLength('hello'); // 5 getLength([1, 2, 3]); // 3 getLength(42); // Error // keyof 제약: 객체의 키만 받도록 function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, name: 'codesche', email: 'a@b.com' }; getProperty(user, 'name'); // string getProperty(user, 'phone'); // Error
TypeScript
복사

제네릭 유틸리티 함수

// 배열 중 특정 키로 그룹화 function groupBy<T>(arr: T[], key: keyof T): Record<string, T[]> { return arr.reduce((acc, item) => { const groupKey = String(item[key]); acc[groupKey] = acc[groupKey] ?? []; acc[groupKey].push(item); return acc; }, {} as Record<string, T[]>); } const users = [ { name: 'Alice', role: 'admin' }, { name: 'Bob', role: 'user' }, { name: 'Carol', role: 'admin' }, ]; groupBy(users, 'role'); // { admin: [{...}, {...}], user: [{...}] }
TypeScript
복사

8. 함수 타입

// 기본 function add(a: number, b: number): number { return a + b; } const add = (a: number, b: number): number => a + b; // 선택적 매개변수 function greet(name: string, greeting?: string): string { return `${greeting ?? 'Hello'}, ${name}!`; } // 기본값 매개변수 function createUser(name: string, role: string = 'user') { /* ... */ } // 나머지 매개변수 function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); } // 함수 오버로드 — 입력에 따라 다른 반환 타입 function parse(value: string): number; function parse(value: number): string; function parse(value: string | number): string | number { return typeof value === 'string' ? parseInt(value) : String(value); } const num = parse('42'); // number const str = parse(42); // string
TypeScript
복사

9. 유틸리티 타입 (Utility Types)

interface User { id: number; name: string; email: string; password: string; role: 'admin' | 'user'; } // Partial<T>: 모든 프로퍼티를 선택적으로 type UserUpdate = Partial<User>; // Required<T>: 모든 프로퍼티를 필수로 type StrictUser = Required<User>; // Readonly<T>: 읽기 전용 type ImmutableUser = Readonly<User>; // Pick<T, K>: 특정 키만 선택 type UserPreview = Pick<User, 'id' | 'name'>; // Omit<T, K>: 특정 키 제외 type PublicUser = Omit<User, 'password'>; // Record<K, V>: 키-값 객체 타입 type RolePermissions = Record<'admin' | 'user', string[]>; // Exclude<T, U>: T에서 U를 제외 type NonAdmin = Exclude<'admin' | 'user' | 'guest', 'admin'>; // 'user' | 'guest' // Extract<T, U>: T에서 U만 추출 type OnlyAdmin = Extract<'admin' | 'user' | 'guest', 'admin' | 'user'>; // 'admin' | 'user' // NonNullable<T>: null, undefined 제외 type SafeString = NonNullable<string | null | undefined>; // string // ReturnType<T>: 함수 반환 타입 추출 type AddResult = ReturnType<typeof add>; // number // Parameters<T>: 함수 매개변수 타입 추출 type AddParams = Parameters<typeof add>; // [number, number]
TypeScript
복사

10. 타입 가드와 Narrowing

// typeof 가드 function process(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); } return value.toFixed(2); } // instanceof 가드 function handleError(err: Error | TypeError) { if (err instanceof TypeError) { console.log('타입 에러:', err.message); } else { console.log('일반 에러:', err.message); } } // in 연산자 가드 interface Cat { meow(): void; } interface Dog { bark(): void; } function speak(animal: Cat | Dog) { if ('meow' in animal) { animal.meow(); } else { animal.bark(); } } // 커스텀 타입 가드 (Type Predicate) function isString(value: unknown): value is string { return typeof value === 'string'; } // 단언 함수 (Assertion Function) function assertDefined<T>(value: T | null | undefined): asserts value is T { if (value == null) throw new Error('Value가 null입니다'); }
TypeScript
복사

11. 클래스(Class)와 타입

abstract class Animal { constructor( public name: string, // public: 외부 접근 가능 protected age: number, // protected: 자신 + 자식 private readonly id: string, // private: 자신만, 읽기 전용 ) {} abstract speak(): string; describe(): string { return `${this.name}, ${this.age}`; } } class Dog extends Animal { constructor(name: string, age: number, private breed: string) { super(name, age, crypto.randomUUID()); } speak(): string { return `${this.name}: 멍!`; } } // 인터페이스 구현 interface Serializable { serialize(): string; } class User implements Serializable { constructor( public readonly id: number, public name: string, private password: string, ) {} serialize(): string { return JSON.stringify({ id: this.id, name: this.name }); } }
TypeScript
복사

12. 고급 타입 활용

Mapped Types

// 커스텀 Partial 구현 type MyPartial<T> = { [K in keyof T]?: T[K]; }; // 모든 값을 string으로 변환 type Stringify<T> = { [K in keyof T]: string; }; // 수정 불가 버전 type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; };
TypeScript
복사

Conditional Types

type IsString<T> = T extends string ? 'yes' : 'no'; type A = IsString<string>; // 'yes' type B = IsString<number>; // 'no' // Flatten: 배열이면 원소 타입 추출 type Flatten<T> = T extends (infer U)[] ? U : T; type Str = Flatten<string[]>; // string type Num = Flatten<number>; // number // Promise 반환 타입 탈기 type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type Result = UnwrapPromise<Promise<string>>; // string
TypeScript
복사

Template Literal Types

type EventName = 'click' | 'focus' | 'blur'; type HandlerName = `on${Capitalize<EventName>}`; // 'onClick' | 'onFocus' | 'onBlur' // API 엔드포인트 타입 생성 type HttpMethod = 'get' | 'post' | 'put' | 'delete'; type ApiPath = '/users' | '/orders' | '/products'; type ApiEndpoint = `${Uppercase<HttpMethod>} ${ApiPath}`; // 'GET /users' | 'POST /users' | ... | 'DELETE /products'
TypeScript
복사

13. 실무 패턴

React 컴포넌트 Props 타입

interface ButtonProps { label: string; variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; onClick: () => void; } const Button: React.FC<ButtonProps> = ({ label, variant = 'primary', size = 'md', disabled = false, onClick, }) => ( <button className={`btn btn-${variant} btn-${size}`} disabled={disabled} onClick={onClick} > {label} </button> ); // 제네릭 컴포넌트 interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; keyExtractor: (item: T) => string; } function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { return ( <ul> {items.map(item => ( <li key={keyExtractor(item)}>{renderItem(item)}</li> ))} </ul> ); }
TypeScript
복사

타입 안전한 API 호출

interface ApiResponse<T> { data: T; status: number; message: string; } async function fetchApi<T>(url: string): Promise<ApiResponse<T>> { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP error: ${res.status}`); return res.json() as Promise<ApiResponse<T>>; } // 반환 타입이 자동 추론됨 const { data: user } = await fetchApi<User>('/api/users/1'); console.log(user.name); // string // Zod를 활용한 런타임 검증 (실무 최강 패턴) import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); type User = z.infer<typeof UserSchema>; // 컴파일 타입 자동 생성 const parsed = UserSchema.parse(await res.json()); // 런타임 검증
TypeScript
복사

정리

주제
핵심
기본 타입
string, number, boolean, null, undefined, unknown, never
객체 형태
interface (OOP) vs type (복합 타입)
연산
Union(`\
제네릭
재사용 가능한 타입, extends 제약, keyof
유틸리티 타입
Partial, Pick, Omit, Record, ReturnType
Narrowing
typeof, instanceof, in, 커스텀 타입 가드
고급 타입
Mapped, Conditional, Template Literal
TypeScript는 타입을 얼마나 올바르게 설계하느냐에 따라 컴파일러가 오류를 얼마나 잊어주는지가 결정됨. any 없이도 충분한 코드를 작성하는 습관을 들이는 게 포인트.