시작하기
Hello World & 컴파일
TypeScript 파일은 .ts 확장자를 사용합니다. tsc 컴파일러는 TS를 JS로 트랜스파일하며, 런타임에 모든 타입 어노테이션을 지웁니다. 최대 타입 안전을 위해 --strict를 사용하세요. ts-node나 bun은 별도의 컴파일 단계 없이 .ts 파일을 직접 실행할 수 있습니다.
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);
// Compile to JavaScript (type annotations erased):
// tsc hello.ts -> hello.js
// tsc --strict hello.ts // enable all strict checks
// tsc --watch hello.ts // recompile on change
// Run directly with ts-node or bun:
// ts-node hello.tstsconfig.json
tsconfig.json은 TypeScript 컴파일러를 설정합니다. 'strict: true'는 noImplicitAny, strictNullChecks, strictFunctionTypes 등을 활성화합니다. 'target'은 출력 JS 버전을 제어합니다. 'esModuleInterop'은 Node의 내장 모듈과 같은 CommonJS 모듈에서 기본 임포트를 활성화합니다.
{
"compilerOptions": {
"target": "ES2020", // JS version to emit
"module": "ESNext", // module system
"strict": true, // enable all strict checks
"outDir": "./dist", // output directory
"rootDir": "./src", // source root
"esModuleInterop": true, // allow default imports from CJS
"skipLibCheck": true, // skip .d.ts type checking
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}타입 어노테이션 & 타입 추론
타입 어노테이션은 변수의 타입을 명시적으로 지정합니다. TypeScript는 값에서 타입을 추론할 수도 있습니다. 함수 시그니처와 공개 API에는 명시적 어노테이션을 사용하고, 지역 변수에는 추론에 의존하세요. 'any'는 피하세요 — 타입 검사를 완전히 건너뜁니다.
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];
// Type inference (let TS figure it out)
let age = 30; // inferred as number
let items = [1, 2, 3]; // inferred as number[]
let mixed = [1, "a"]; // inferred as (string | number)[]
// any disables type checking (avoid!)
let anything: any = 42;
anything = "string"; // no error엄격 모드 검사
엄격 모드는 중요한 검사를 활성화합니다: strictNullChecks (null/undefined를 다른 타입에 할당 불가), noImplicitAny (매개변수에 타입 필요), strictPropertyInitialization (클래스 필드 초기화 필요). 필드가 나중에 설정될 것이라 확신할 때 '!' (확정 할당)을 사용하세요.
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode
// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any
// strictPropertyInitialization
class User {
name: string; // Error: not initialized
constructor() {}
}
// Fix: initialize or use definite assignment
class Fixed {
name!: string; // definite assignment assertion
age: number = 0;
}선언 파일 (.d.ts)
선언 파일(.d.ts)은 TypeScript 정의가 없는 JavaScript 라이브러리에 타입을 제공합니다. 'declare'는 변수/함수가 런타임에 존재함을 컴파일러에게 알립니다. 인기 있는 라이브러리를 위해 DefinitelyTyped의 @types 패키지를 사용하세요 (예: @types/node, @types/react).
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
export function greet(name: string): string;
export const version: string;
}
// global.d.ts - extend global scope
declare global {
interface Window {
myApp: { version: string };
}
}
// Usage in .ts files:
// import { greet } from "my-lib"; // now typed
// Install @types packages: npm i -D @types/node @types/react기본 타입
원시 타입 & 특수 타입
TypeScript 원시 타입: string, number, boolean, bigint, symbol. 'void'는 함수가 값을 반환하지 않음을 나타냅니다. 'never'는 절대 발생하지 않는 값을 나타냅니다 — 예외를 던지거나 영원히 실행되는 함수입니다. switch 문에서 철저한 검사를 위해 'never'를 사용하세요.
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");
// void: function returns nothing
function log(msg: string): void { console.log(msg); }
// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }배열 & 튜플
배열은 T[] 또는 Array<T> 구문을 사용합니다. ReadonlyArray는 변경을 방지합니다. 튜플은 각 인덱스에 특정 타입을 갖는 고정 길이 배열입니다 — 키-값 쌍이나 CSV 형식 데이터에 유용합니다. 레이블이 있는 튜플은 명명된 위치로 가독성을 향상합니다.
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];
// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable
// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1]; // number
// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];열거형(Enum)
열거형은 명명된 상수 집합을 정의합니다. 문자열 열거형은 디버깅에 권장됩니다 (출력에서 값이 읽기 쉬움). 숫자 열거형은 역방향 매핑을 지원합니다. 'const enum'은 컴파일 타임에 지워집니다 (인라인됨) — 런타임 비용이 없습니다. 간단한 경우에는 유니언 타입을 선호하세요.
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up; // 0
// String enum (recommended for readability)
enum Status {
Pending = "PENDING",
Success = "SUCCESS",
Failed = "FAILED",
}
// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"
// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;any vs unknown vs never
'any'는 모든 타입 검사를 비활성화합니다 — 피하세요. 'unknown'은 타입 안전한 대안입니다: 사용하기 전에 반드시 좁혀야 합니다 (typeof, instanceof 사용). 'never'는 절대 발생하지 않는 값을 나타내며, switch 문에서 컴파일 타임에 누락된 케이스를 잡기 위한 철저성 검사에 사용됩니다.
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string"; // OK
a.toUpperCase(); // OK (no check, may fail at runtime)
// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
u.toUpperCase(); // OK after narrowing
}
// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
switch (s) {
case "circle": return Math.PI;
case "square": return 1;
default:
const _exhaustive: never = s; // Error if case missing
}
}타입 어설션
타입 어설션은 컴파일러에게 '내가 타입을 알아, 믿어'라고 알립니다. 'as' 구문을 사용하세요. non-null 어설션(!)은 TS에 값이 null/undefined가 아님을 알립니다. 'as const'는 모든 속성을 읽기 전용 리터럴로 만듭니다 — 설정 객체와 Redux 액션 타입에 유용합니다. 어설션은 런타임 동작을 변경하지 않습니다.
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;
// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi"; // el is HTMLElement, not null
// Double assertion (for unsafe casts)
let value = "42" as unknown as number;
// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonly리터럴 & 유니언 타입
리터럴 타입은 값을 특정 문자열, 숫자, 또는 불리언으로 제한합니다. 유니언과 결합하여 방향이나 HTTP 메서드 같은 정밀한 타입을 만듭니다. 템플릿 리터럴 타입(TS 4.1+)은 다른 타입에서 문자열 타입을 구축합니다 — 타입 안전한 키와 이벤트 이름 생성에 강력합니다.
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left"; // OK
// direction = "sideways"; // Error
// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4; // OK
// Boolean literal
let flag: true = true;
// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"인터페이스 & 객체
인터페이스 기본
인터페이스는 객체의 형태를 설명합니다. '?'는 선택적 속성(undefinded일 수 있음)을 표시합니다. 'readonly'는 초기화 후 재할당을 방지합니다. 인터페이스는 컴파일 타임 전용입니다 — 출력 JavaScript에서 지워집니다. 객체와 클래스에 대한 계약을 정의하는 데 사용하세요.
interface User {
id: number;
name: string;
email?: string; // optional property
readonly createdAt: Date; // immutable
}
const user: User = {
id: 1,
name: "Alice",
createdAt: new Date(),
};
// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefined인덱스 시그니처
인덱스 시그니처는 주어진 타입의 임의 키를 가진 객체를 허용합니다. 모든 속성 값은 인덱스 타입에 할당 가능해야 합니다. 딕셔너리, 캐시, 동적 데이터에 유용합니다. 알려진 속성과 결합하여 추가 옵션이 있는 타입화된 설정에 사용하세요.
// Index signature: arbitrary string keys
interface StringMap {
[key: string]: string;
}
const dict: StringMap = {
name: "Alice",
city: "NYC",
// count: 42, // Error: value must be string
};
// Mixed: known + index signature
interface Config {
name: string;
[key: string]: string | number;
}
// Readonly index signature
interface ReadonlyMap {
readonly [key: string]: number;
}인터페이스 확장
인터페이스는 하나 이상의 다른 인터페이스를 확장하여 멤버를 결합할 수 있습니다. 이를 통해 컴포지션과 코드 재사용이 가능합니다. 클래스와 달리 인터페이스는 다중 상속을 지원합니다. 인터페이스를 구현할 때, 클래스는 모든 필수 멤버를 제공해야 합니다.
interface Animal {
name: string;
eat(): void;
}
interface Pet extends Animal {
owner: string;
play(): void;
}
interface Swimmer {
swim(): void;
}
// Multiple inheritance
interface Duck extends Pet, Swimmer {
quack(): void;
}
const duck: Duck = {
name: "Donald",
owner: "Walt",
eat() {},
play() {},
swim() {},
quack() {},
};인터페이스의 함수 타입
인터페이스는 함수 시그니처를 설명할 수 있어 타입 안전한 콜백이 가능합니다. 하이브리드 인터페이스(호출 가능 + 속성)는 메서드도 있는 jQuery 스타일 함수에 사용됩니다. 이 패턴은 도우미가 첨부된 함수를 반환하는 라이브러리에서 흔합니다.
interface SearchFn {
(source: string, keyword: string): boolean;
}
const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true
// Interface with mixed members (hybrid)
interface Counter {
(start: number): void; // callable
count: number; // property
reset(): void; // method
}
// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };인터페이스 vs 타입 별칭
인터페이스는 선언 병합(같은 이름의 인터페이스 결합), 더 나은 에러 메시지를 지원하며 객체/클래스 형태에 권장됩니다. 타입 별칭은 더 유연하지만(유니언, 원시 타입, 튜플 표현 가능) 병합할 수 없습니다. 확장 가능한 API에는 인터페이스를, 유니언과 계산된 타입에는 타입 별칭을 사용하세요.
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };
// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;
// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };
// Use interface for objects/classes, type for unions/aliases옵셔널 체이닝 & 널 병합
옵셔널 체이닝(?.)은 중첩된 속성에 안전하게 접근합니다 — 어떤 연결고리라도 null/undefined이면 예외를 던지는 대신 undefined를 반환합니다. 널 병합(??)은 null/undefined에 대해서만 기본값을 제공합니다 (0이나 ''이 아님). 이 연산자들은 장황한 null 검사 코드를 크게 줄입니다.
interface User {
profile?: {
address?: {
city?: string;
};
};
}
const user: User = {};
// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined
// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";
// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky
// Optional method call
const result = user.profile?.address?.city?.toUpperCase();타입 별칭 & 유니언
타입 별칭
타입 별칭은 유니언, 인터섹션, 원시 타입, 제네릭을 포함한 모든 타입에 대한 명명된 참조를 만듭니다. 인터페이스와 달리 별칭은 병합되거나 확장될 수 없지만, 더 유연합니다. 유니언, 튜플, 유틸리티 타입에는 별칭을 사용하고, 객체 형태에는 인터페이스를 사용하세요.
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };
// Generic alias
type Container<T> = { value: T };
// Function type alias
type Handler<T> = (event: T) => void;
// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);유니언 타입
유니언 타입(A | B)은 값이 여러 타입 중 하나일 수 있게 합니다. TypeScript는 typeof, instanceof, 또는 in 검사를 사용하여 조건 블록 내에서 타입을 좁힙니다. 참고: (string | number)[]는 string[] | number[]와 다릅니다 — 전자는 혼합 배열, 후자는 모두 문자열이거나 모두 숫자입니다.
// Union: value can be one of several types
type ID = string | number;
function display(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // narrowed to string
} else {
console.log(id.toFixed(2)); // narrowed to number
}
}
display("abc"); // ABC
display(42); // 42.00
// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];인터섹션 타입
인터섹션 타입(A & B)은 여러 타입의 모든 멤버를 결합합니다 — 결과는 모든 타입을 만족해야 합니다. 믹스인, 컴포지션, 유틸리티 타입 병합에 유용합니다. 유니언(OR)과 달리 인터섹션은 AND입니다: 값은 모든 타입의 모든 속성을 가져야 합니다.
// Intersection: combine multiple types into one
interface BusinessPartner {
name: string;
credit: number;
}
interface Identity {
id: number;
email: string;
}
type Employee = BusinessPartner & Identity;
const emp: Employee = {
name: "Alice",
credit: 1000,
id: 1,
email: "[email protected]",
};
// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing props널러블 타입
엄격 모드에서 null과 undefined는 다른 타입에 할당할 수 없습니다 — 유니언(string | null)으로 명시적으로 포함해야 합니다. 선택적 매개변수(param?)는 암시적으로 T | undefined입니다. 안전한 기본값에는 ??를, non-null 단언에는 !(드물게 사용)을 사용하세요.
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice"; // OK
// Optional parameters are implicitly | undefined
function greet(name?: string) {
// name is string | undefined
console.log(name ?? "Guest");
}
// Return type can be null
function find(id: number): string | null {
return id === 1 ? "Alice" : null;
}
// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"Keyof & Typeof 연산자
'keyof T'는 타입 T의 키를 문자열 리터럴 유니언으로 추출합니다. 'typeof x'는 값의 타입을 추출합니다(객체에서 추론하는 데 유용). 'keyof typeof obj'는 둘을 결합하여 기존 객체의 키를 얻습니다 — Redux 액션 타입과 타입 안전한 속성 접근자에서 흔합니다.
interface User {
id: number;
name: string;
email: string;
}
// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"
function getProp(obj: User, key: keyof User) {
return obj[key];
}
// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }
// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"매핑된 타입
매핑된 타입은 키를 순회하며 타입을 변환합니다. Readonly, Partial, Pick 같은 내장 유틸리티가 매핑된 타입입니다. + 와 - 수정자를 사용하여 readonly 또는 선택적을 추가/제거하세요. 키 리매핑(TS 4.1+)은 템플릿 리터럴 타입을 사용하여 키 이름을 바꿉니다 — getter/setter 타입 생성에 강력합니다.
// Map over keys to create a new type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User { id: number; name: string; }
type ReadonlyUser = Readonly<User>; // all readonly
type OptionalUser = Optional<User>; // all optional
// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };
// Key remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};함수
함수 타입 & 시그니처
TypeScript는 함수 매개변수와 반환 값에 타입 어노테이션을 추가합니다. 반환 타입은 종종 추론 가능하지만, 공개 API에는 명시적 어노테이션이 권장됩니다. 기본 매개변수는 대체 값으로 인수를 선택적으로 만듭니다. 함수가 아무것도 반환하지 않을 때 void를 사용하세요.
// Named function with types
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;
// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;
// Void return (no return value)
function log(msg: string): void { console.log(msg); }
// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
return `${greeting}, ${name}!`;
}
greet("Alice"); // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"나머지 매개변수 & 튜플
나머지 매개변수(...args)는 여러 인수를 배열로 수집합니다. TypeScript는 이를 T[] 또는 고정 길이 가변 함수를 위 한 튜플로 타입화합니다. 스프레드 연산자(...)는 반대로 동작합니다 — 배열을 개별 인수로 확장합니다. 튜플 나머지 타입은 정밀한 가변 시그니처를 가능하게 합니다.
// Rest parameters (variadic)
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
console.log(name, scores);
}
// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));
// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
const [s, n, b] = args;
}함수 오버로드
함수 오버로드는 같은 함수에 여러 타입 시그니처를 제공하여, 입력에 따른 정밀한 반환 타입을 가능하게 합니다. 구현 시그니처는 호출자에게 숨겨집니다. 오버로드는 위에서 아래로 해결됩니다 — 더 구체적인 시그니처를 먼저 배치하세요. jQuery와 lodash 같은 라이브러리에서 흔합니다.
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
if (typeof input === "string") {
return input.split(",");
}
return [input, input * 2];
}
const strs = parse("a,b,c"); // string[]
const nums = parse(42); // number[]
// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
return m === undefined
? new Date(yOrTs)
: new Date(yOrTs, m - 1, d);
}this 타입
TypeScript는 'this' 타입을 첫 번째 매개변수로 선언할 수 있게 합니다. 이는 함수가 올바른 컨텍스트로 호출됨을 보장합니다 — 콜백으로 전달되는 메서드에 유용합니다. 화살표 함수는 'this'를 렉시컬하게 캡처하여 .bind()의 필요를 피합니다. 타입이 없는 'this' 에러를 잡으려면 'noImplicitThis'를 사용하세요.
interface Card {
suit: string;
rank: string;
isFaceUp(): boolean;
}
// Explicit 'this' parameter
function format(this: Card): string {
return `${this.rank} of ${this.suit}`;
}
const card: Card = {
suit: "Hearts",
rank: "A",
isFaceUp() { return true; },
format,
};
// 'this' in callbacks with bind
class Handler {
private count = 0;
increment = () => { this.count++; }; // arrow binds this
}콜백 & 고차 함수
TypeScript는 고차 함수(함수를 받거나 반환하는 함수)를 완전히 타입화합니다. 입력과 출력 간의 타입 관계를 보존하려면 제네릭 타입 매개변수(T, U)를 사용하세요. 콜백 타입은 재사용을 위해 타입 별칭으로 정의하는 것이 일반적입니다. 커링(함수 반환)은 완전히 타입 안전합니다.
// Function as parameter
type Callback<T> = (value: T, index: number) => void;
function forEach<T>(arr: T[], cb: Callback<T>): void {
for (let i = 0; i < arr.length; i++) {
cb(arr[i], i);
}
}
forEach(["a", "b"], (v, i) => console.log(i, v));
// Function returning function (curry)
function add(a: number): (b: number) => number {
return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
return arr.map(fn);
}매개변수 구조 분해
TypeScript는 함수 매개변수에서 구조 분해를 지원합니다 — 분해된 형태를 인라인으로 또는 인터페이스를 통해 어노테이션합니다. 인터페이스로 추출하면 가독성과 재사용성이 향상됩니다. 배열/튜플 구조 분해도 작동합니다. 이 패턴은 React 컴포넌트 props와 API 핸들러에서 흔합니다.
// Destructured parameters with types
function createUser({ name, age, email }: {
name: string;
age: number;
email?: string;
}): void {
console.log(name, age, email);
}
createUser({ name: "Alice", age: 30 });
// Extract to interface for reuse
interface UserOpts {
name: string;
age: number;
email?: string;
}
function updateUser({ name, age }: UserOpts): void {}
// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
return [b, a];
}클래스 & OOP
클래스 & 생성자
TypeScript 클래스는 매개변수 속성을 지원합니다 — 생성자 매개변수에 접근 수정자(public/private/protected/readonly)를 붙이면 필드가 자동 생성되고 할당됩니다. 이 약식은 보일러플레이트를 줄입니다. 메서드는 반환 값에 타입 어노테이션을 가질 수 있습니다. 필드는 기본이 public입니다.
class Person {
// Parameter properties (shorthand)
constructor(
public name: string, // auto-creates this.name
private age: number, // private field
readonly id: number, // immutable
) {}
greet(): string {
return `Hi, I'm ${this.name}`;
}
}
const p = new Person("Alice", 30, 1);
console.log(p.name); // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonly접근 수정자
접근 수정자: public(기본, 어디서나), private(클래스만), protected(클래스 + 서브클래스), readonly(불변). TypeScript의 'private'는 컴파일 타임 전용입니다; ES '#' 프라이빗 필드는 런타임에 진정으로 private입니다. 구현 세부사항에는 private을, 확장 지점에는 protected를 사용하세요.
class BankAccount {
public owner: string; // accessible everywhere
private balance: number; // class only
protected rate: number; // class + subclasses
readonly id: string; // immutable after init
#secret: string; // ES private (runtime)
constructor(owner: string) {
this.owner = owner;
this.balance = 0;
this.rate = 0.05;
this.id = crypto.randomUUID();
this.#secret = "hidden";
}
deposit(amount: number): void {
this.balance += amount;
}
}상속 & 추상 클래스
추상 클래스는 직접 인스턴스화할 수 없습니다 — 서브클래스의 기반을 정의합니다. 추상 메서드는 기반 클래스에 구현이 없습니다; 서브클래스가 구현해야 합니다. 상속에는 'extends'를, 부모 생성자 호출에는 'super()'를 사용하세요. 추상 클래스는 다형성을 가능하게 합니다 — 코드가 모든 Shape 서브클래스와 작동할 수 있습니다.
abstract class Shape {
constructor(public color: string) {}
abstract area(): number; // must be implemented
describe(): string {
return `${this.color} shape, area ${this.area()}`;
}
}
class Circle extends Shape {
constructor(color: string, private r: number) {
super(color);
}
area(): number { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
constructor(color: string, private side: number) {
super(color);
}
area(): number { return this.side ** 2; }
}
const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstract인터페이스 & 구현
클래스는 여러 인터페이스를 구현할 수 있습니다(쉼표로 구분). 클래스는 모든 인터페이스 멤버를 제공해야 합니다. extends(단일 상속)와 달리 implements는 여러 계약을 지원합니다. 이것이 TypeScript가 다중 상속과 유사한 동작을 달성하는 방법입니다. 계약은 인터페이스로, 구현은 클래스로 정의하세요.
interface Printable {
toString(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Printable, Comparable<Money> {
constructor(private amount: number) {}
toString(): string {
return `$${this.amount.toFixed(2)}`;
}
compareTo(other: Money): number {
return this.amount - other.amount;
}
}
const a = new Money(10);
const b = new Money(20);
console.log(a.toString()); // "$10.00"
console.log(a.compareTo(b)); // -10게터 & 세터
게터와 세터는 검증, 계 산, 또는 부작용을 위해 속성 접근을 가로챕니다. private 백킹 필드를 사용하세요(관례: 언더스코어 접두사). 게터는 계산된 속성을 가능하게 합니다(섭씨에서 화씨처럼). 세터는 검증을 가능하게 합니다. 일반 속성처럼 접근하세요 — 괄호 없이.
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) throw new Error("Below absolute zero");
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * 5 / 9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throws정적 멤버 & 싱글톤
정적 멤버는 인스턴스가 아닌 클래스에 속합니다 — ClassName.member로 접근합니다. 상수, 유틸리티 함수, 팩토리 메서드에 static을 사용하세요. private 생성자 + static getInstance()가 싱글톤 패턴을 구현합니다. 'as const'는 정적 배열을 리터럴 타입으로 읽기 전용으로 만듭니다.
class Logger {
static instance: Logger;
private logs: string[] = [];
private constructor() {} // prevent direct new
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;
log(msg: string): void {
this.logs.push(msg);
}
}
const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructor제네릭
제네릭 함수
제네릭(<T>)은 타입 안전성을 보존하면서 모든 타입과 작동하는 함수를 작성하게 합니다. 타입 매개변수 T는 호출 시간에 채워지는 자리표시자입니다 — 명시적으로(identity<number>) 또는 인수에서 추론됩니다. 제네릭은 재사용 가능하고 타입 안전한 데이터 구조와 알고리즘을 가능하게 합니다.
// Generic function: preserves type relationship
function identity<T>(value: T): T {
return value;
}
const n = identity<number>(42); // T = number, returns number
const s = identity("hi"); // T inferred as string
// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("Alice", 30); // [string, number]
// Generic with array
function first<T>(arr: T[]): T | undefined {
return arr[0];
}제네릭 클래스
제네릭 클래스(<T>)는 타입 안전한 컨테이너를 만듭니다. 각 인스턴스는 특정 타입을 고정합니다 — Stack<number>는 숫자만 받습니다. 이는 런타임 오버헤드 없이 컴파일 타임에 타입 에러를 잡습니다(제네릭은 지워짐). 컬렉션(Stack, Queue, Map)과 반응형 래퍼(Observable<T>)에서 흔합니다.
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;
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2
const strStack = new Stack<string>();
strStack.push("hello");제네릭 제약
제약(T extends SomeType)은 제네릭이 받을 수 있는 타입을 제한합니다. 'T extends HasLength'는 T가 'length' 속성을 가짐을 보장합니다. 'K extends keyof T'(keyof 제약)는 객체에 키가 존재함을 보장하며, 올바른 값 타입을 반환합니다. 제약은 제네릭에 대한 타입 안전한 속성 접근과 메서드 호출을 가능하게 합니다.
// Constraint: T must have a 'length' property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("hello"); // 5 (string has length)
logLength([1, 2, 3]); // 3 (array has length)
// logLength(42); // Error: number has no length
// Constraint with keyof
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: not a key기본 타입 매개변수
기본 타입 매개변수는 지정되지 않았을 때 대체 타입을 제공합니다. 일반적인 경우가 있는 API에 유용합니다(예: ApiResponse는 기본값이 string). 기본값은 이전 매개변수에 의존할 수 있습니다. 제약과 결합하여(T extends X = DefaultType) 타입 안전한 선택적 제네릭을 사용하세요.
// Default type parameter
interface ApiResponse<T = string> {
data: T;
status: number;
}
const r1: ApiResponse = { data: "hello", status: 200 }; // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 }; // T = number
// Multiple defaults
class Container<T = string, U = number> {
constructor(public a: T, public b: U) {}
}
const c1 = new Container("hi", 42); // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>
// Default with constraint
interface Box<T extends object = { id: number }> {
value: T;
}제네릭 인터페이스 & 타입
제네릭 인터페이스와 타입 별칭은 재사용 가능하고 타입 안전한 계약을 만듭니다. Repository<T>는 일관된 API로 데이터 접근을 추상화합니다. Result<T, E>는 예외 없는 에러 처리를 위한 구별된 유니언입니다. 기본 타입 매개변수(E = Error)는 일반적인 경우의 보일러플레이트를 줄입니다.
// Generic interface
interface Repository<T> {
findById(id: string): Promise<T>;
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Implement with concrete type
class UserRepo implements Repository<User> {
async findById(id: string): Promise<User> { /* ... */ }
async save(user: User): Promise<void> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}
// Generic type alias with conditional
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };조건부 타입
조건부 타입(T extends U ? X : Y)은 타입 수준의 if 문입니다. 'infer R'은 다른 타입 내에서 타입을 추출합니다(예: 함수의 반환 타입). 조건부 타입은 유니언에 분배됩니다 — ToArray<string | number>는 string[] | number[]가 됩니다. Exclude, Extract, NonNullable 같은 내장 유틸리티가 이를 사용합니다.
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
// Extract return type
type AsyncReturnType<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;고급 타입
유틸리티 타입
TypeScript는 일반적인 변환을 위한 내장 유틸리티 타입을 제공합니다: Partial(모두 선택적), Pick(키 선택), Omit(키 제외), Record(키-값 맵), Required(선택적 제거), ReturnType(함수 반환), Parameters(함수 매개변수를 튜플로). 이들은 반복적인 타입 정의를 제거합니다.
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };
// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;
// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;
// Record: key-value map
type UserMap = Record<string, User>;
// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;
// ReturnType: function return type
type R = ReturnType<() => string>; // string
// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]템플릿 리터럴 타입
템플릿 리터럴 타입(TS 4.1+)은 다른 타입을 보간하여 문자열 타입을 구축합니다. 유니언과 결합하여 문자열의 데카르트 곱을 생성합니다. Capitalize/Uppercase가 대소문자를 변환합니다. 타입 안전한 이벤트 이름, getter/setter 생성, API 라우트 타이핑에 사용하세요. 매핑된 타입의 'as' 절은 키 이름 변경을 가능하게 합니다.
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">; // "Foo"
// Getter names from keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"infer 키워드
'infer' 키워드는 조건부 타입의 'extends' 절 내에 타입 변수를 선언하여, 재사용을 위해 타입을 캡처합니다. ReturnType, Parameters, Awaited 같은 유틸리티 타입의 기반입니다. infer를 사용하여 복잡한 구조(배열, 프로미스, 함수)에서 타입을 추출하세요 — 수동으로 분해할 필요 없이.
// Extract return type of a function
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean
// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string
// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number
// Extract first parameter
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // string구별된 유니언
구별된 유니언(태그된 유니언)은 공유 리터럴 필드('type', 'kind', 'tag')를 사용하여 변형을 구분합니다. TypeScript는 각 switch 케이스에서 타입을 좁혀 케이스별 필드에 접근하게 합니다. 'never' 기본값은 철저성 검사를 가능하게 합니다 — 새 케이스를 추가하면, 처리할 때까지 컴파일러가 에러를 냅니다. Redux 리듀서와 상태 머신에 필수적입니다.
// Discriminated union: shared 'type' (or 'kind') field
type Action =
| { type: "ADD_TODO"; text: string }
| { type: "DELETE_TODO"; id: number }
| { type: "TOGGLE_TODO"; id: number };
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [...state, { id: Date.now(), text: action.text }];
case "DELETE_TODO":
return state.filter(t => t.id !== action.id);
case "TOGGLE_TODO":
return state.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
);
default:
const _: never = action; // exhaustiveness check
return state;
}
}타입 가드: typeof & instanceof
타입 가드는 런타임에 타입을 좁힙니다. 'typeof'는 원시 타입(string, number, boolean, symbol, bigint, undefined, function, object)에 작동합니다. 'instanceof'는 클래스/생성자 프로토타입을 검사합니다. Array.isArray()는 타입화된 배열로 좁힙니다. 이들은 내장 가드입니다 — 커스텀 코드가 필요 없습니다. TypeScript는 각 브랜치에서 좁혀진 타입을 추적합니다.
// typeof: narrow primitives
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string
}
return value.toFixed(2); // number
}
// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }
function speak(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // Dog
} else {
pet.meow(); // Cat
}
}
// Array.isArray
function flatten(arr: (number | number[])[]) {
return arr.flatMap(x =>
Array.isArray(x) ? x : [x]
);
}커스텀 타입 가드 & in 연산자
'in' 연산자는 객체에 속성이 존재하는지 검사하여, 그것을 가진 타입으로 좁힙니다. 타입 술어(x is T)는 불리언을 반환하지만 타입도 좁히는 커스텀 가드 함수입니다. 외부 데이터(JSON.parse, API 응답)의 안전한 파싱을 위해 'unknown'을 입력 타입으로 사용하세요. 술어는 재사용 가능하고 조합 가능한 타입 검사를 가능하게 합니다.
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// Type predicate: custom guard function
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: any): x is { name: string; age: number } {
return x && typeof x.name === "string" && typeof x.age === "number";
}
const val: unknown = "hello";
if (isString(val)) {
console.log(val.toUpperCase()); // narrowed to string
}데이터 구조
배열 & ReadonlyArray
TypeScript 배열은 타입화됩니다 — map, filter, reduce 같은 메서드는 요소 타입을 보존합니다. 불변성을 위해 readonly T[] 또는 ReadonlyArray<T>를 사용하세요. 튜플은 고정 길이와 타입화된 위치를 가집니다. 배열 구조 분해와 스프레드는 완전히 타입 안전합니다. 타입 시스템은 인덱스 범위 초과와 잘못된 타입 할당을 잡습니다.
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];
// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly
// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];
// Array methods are typed
const doubled = nums.map(n => n * 2); // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number
// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];객체 & Record
Record<K, V>는 K 타입의 키와 V 타입의 값을 가진 타입화된 맵을 만듭니다. Partial<T>는 모든 속성을 선택적으로 만듭니다 — 업데이트/패치 작업에 이상적입니다. Object.entries/keys/values는 타입화된 배열을 반환합니다. 기존 타입에서 집중된 타입을 파생하려면 Pick과 Omit을 사용하여 타입을 DRY하게 유지하세요.
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };
// Record: typed key-value map
const scores: Record<string, number> = {
math: 90,
science: 85,
};
// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };
// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;
// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores); // string[]
const values = Object.values(scores); // number[]Map & Set
Map과 Set은 완전한 TypeScript 지원을 가진 ES6 컬렉션입니다. Map 키는 모든 타입이 될 수 있습니다(객체와 달리 키를 문자열로 강제하지 않음). Set은 고유한 값을 저장합니다. WeakMap/WeakSet은 키의 가비지 컬렉션을 허용합니다 — DOM 요소나 객체에 메타데이터를 첨부하면서 GC를 방해하지 않을 때 유용합니다.