8편. 개발자용 핵심 문법 & 실전 타입 설계 핸드북
📚 목차
1. 기본 타입 시스템 (Core Type System)
2. 함수 (Functions)
3. 클래스와 객체지향 프로그래밍 (Classes and OOP)
4. 제네릭 (Generics) : 타입 변수
5. 고급 타입 조작 (Advanced Type Manipulation)
6. 비동기 프로그래밍 (Asynchronous Programming)
7. 모듈 시스템 (ESM for Node.js) : export, import

1. 기본 타입 시스템 (Core Type System)
🔷 1. 원시 타입 (Primitive Types)
| 타입 | 설명 | 예시 |
| string | 문자열 | const userName: string = "TypeScript Learner"; const greeting: string = `Hello, ${userName}!`; |
| number | 숫자(정수/실수) | const decimal: number = 25.5; const hex: number = 0xf00d; const binary: number = 0b1010; |
| boolean | 논리값( true/false) | let isActive: boolean = true; |
| null | 의도적인 값 없음 strictNullChecks일 때 보통 유니온으로 사용 |
let data: string | null = null; |
| undefined | 값이 할당되지 않음 strictNullChecks: true면 단일 타입에 바로 대입 불가 |
let result: number | undefined = undefined; |
| symbol | 고유하고 불변한 값 같은 설명 문자열이어도 매번 고유 |
const uniqueId: symbol = Symbol("id"); console.log(uniqueId === anotherId); // 출력: false |
| bigint | 매우 큰 정수, n 접미사 사용 Number 안전 범위 초과 정수 처리 |
const bigNumber: bigint = 9007199254740991n + 1n; |
🔷 2. 특수 타입 (Special Types)
| 타입 | 설명 | 예시 |
| any | 모든 타입을 허용(사용 지양) | let v: any = 1; v.toUpperCase(); // 컴파일 OK, 런타임 에러 가능 |
| void | 함수가 값을 반환하지 않음 | function log(msg: string): void { console.log(msg); } |
| never | 절대 도달할 수 없는 코드 경로 무한 루프, 항상 예외를 던지는 함수, 철저한 switch exhaustiveness에 사용 |
function fail(): never { throw new Error("x"); } |
| unknown | 사용 전 반드시 타입 검사/좁히기 필요 | let x: unknown = "hi"; if (typeof x === "string") x.toUpperCase(); |
▸ unknown의 안전한 사용법(Type Narrowing)
let userInput: unknown = "Hello TS";
userInput = 42;
userInput = { message: "Safe Data" };
// 1. unknown 타입의 값은 바로 사용하거나 속성에 접근할 수 없습니다.
// console.log(userInput.message); // 오류: 'userInput'의 형식이 'unknown'입니다.
// 2. 값을 사용하려면 반드시 타입 가드(Type Guard)를 사용하여 타입을 좁혀야 합니다.
// A. typeof 가드
if (typeof userInput === 'string') {
// 이 블록 안에서 userInput은 string 타입으로 안전하게 취급됩니다.
console.log(userInput.toUpperCase());
}
// B. instanceOf 가드 (객체 타입을 좁힐 때)
class CustomError extends Error {}
let errorInput: unknown = new CustomError("Failed");
if (errorInput instanceof CustomError) {
// 이 블록 안에서 errorInput은 CustomError 타입입니다.
console.log(`Error Name: ${errorInput.name}`);
}
🔷 3. 컬렉션 타입 (Collection Types)
▸ 배열 (Array)
두 가지 방식으로 정의할 수 있으며, 배열에 저장될 항목의 타입이 통일되어야 합니다.
// 1. 타입 뒤에 [] 사용 (가장 일반적인 형태)
const fruits: string[] = ["apple", "banana", "cherry"];
// 2. 제네릭 Array<T> 사용
const scores: Array<number> = [90, 85, 95];
// 배열에 다른 타입이 할당되면 오류 발생
// fruits.push(123); // 🚨 오류: number 타입은 string[] 배열에 할당될 수 없습니다.
▸ 튜플 (Tuple)
정해진 순서와 정해진 개수의 항목 타입을 미리 선언하는 배열의 특별한 형태입니다.
// 튜플 정의: [string, number, boolean] 순서를 지켜야 합니다.
let userProfile: [string, number, boolean];
// 할당: 순서와 개수가 일치해야 함
userProfile = ["Alice", 30, true];
// userProfile = [30, "Alice", true]; // 🚨 오류: 타입 순서가 다름
// userProfile[0]은 string 타입입니다.
console.log(`Name: ${userProfile[0]}`);
// userProfile[1]은 number 타입입니다.
const age: number = userProfile[1];
// 튜플은 배열 메서드를 허용하지만, 새 요소를 추가할 때는 유니온 타입으로 취급됩니다.
userProfile.push("Manager"); // ⚠️ push는 허용되나, 타입 안전성이 약해질 수 있음
🔷 4. 객체 타입 정의: 인터페이스 vs 타입 별칭
| 구분 | interface | type alias |
| 확장 (Extension) | extends 키워드 사용 | &(인터섹션) 연산자 사용 |
| 선언적 확장 (Merging) | 가능 (동일 이름으로 선언 시 자동 병합) | 불가능 (동일 이름 선언 시 오류) |
| 활용 범위 | 객체 타입, 클래스 구현 | 모든 타입 (원시, 유니온, 튜플, 객체 등) |
| 주요 시나리오 | 객체 구조 정의, 클래스 타입 계약, 라이브러리/프레임워크의 타입 정의 |
유니온/튜플 등 복잡한 타입에 새 이름 부여, 타입 매핑 |
▸ 인터페이스 (interface)
주로 객체의 모양을 정의하고 클래스를 구현하거나 확장할 때 사용됩니다.
interface Point {
x: number;
y: number;
}
interface Point3D extends Point { // 인터페이스 확장 (extends)
z: number;
}
const p: Point3D = { x: 10, y: 20, z: 30 };
// *특징: 선언적 확장 (Declaration Merging)
// 동일한 이름의 인터페이스를 여러 번 선언하면 자동으로 합쳐집니다.
interface User {
id: number;
}
interface User {
name: string;
}
const mergedUser: User = { id: 1, name: "Charlie" }; // id와 name을 모두 가짐
▸ 타입 별칭 (type alias)
새로운 타입 이름을 생성하며, 객체 외에도 원시 타입, 유니온, 튜플, 함수 시그니처 등 거의 모든 타입에 사용될 수 있습니다.
type ID = string | number; // 유니온 타입에 별칭 부여
type Coordinate = [number, number]; // 튜플 타입에 별칭 부여
type Employee = {
id: ID;
department: string;
};
// 타입 별칭은 인터섹션(&)을 사용하여 결합할 수 있습니다.
type Manager = Employee & {
reportsTo: ID;
};
const manager: Manager = {
id: 101,
department: "Sales",
reportsTo: 100
};
// *특징: 확장 불가 및 선언적 확장 불가
// type Manager extends Employee ... (불가)
// 동일 이름으로 type 선언 시 오류 발생 (Declaration Merging 불가)
🔷 5. 조합 타입 (Combination Types)
▸ 유니온 (Union Type, |) : 두 개 이상의 타입 중 하나일 수 있음을 나타냅니다.
// 값은 string이거나 number여야 합니다.
type Numeric = string | number;
function printID(id: Numeric) {
console.log(`ID: ${id}`);
// 유니온 타입의 값은 공통된 속성(예: toString())만 접근 가능합니다.
// 타입 가드를 통해 타입을 좁혀야 합니다.
if (typeof id === 'string') {
console.log(id.toUpperCase()); // string 타입일 때만 가능
}
}
printID(108);
printID("TS_101");
▸ 인터섹션 (Intersection Type, &) :
두 개 이상의 타입을 모두 만족하는 타입을 만듭니다.
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
// IntersectedPerson은 name 속성과 age 속성을 모두 가져야 합니다.
type IntersectedPerson = HasName & HasAge;
const person: IntersectedPerson = {
name: "David",
age: 45
// height: 180 // 인터섹션에 없는 속성을 추가하면 오류
};
🔷 6. 리터럴 타입 (Literal Types) :
특정 원시 값 자체를 타입으로 지정합니다. 값 자체가 타입이 되므로, 변수에 오직 그 리터럴 값만 할당할 수 있습니다.
// 1. 문자열 리터럴 타입
type Direction = "up" | "down" | "left" | "right";
let move: Direction;
move = "up";
// move = "forward"; // 🚨 오류: 'forward' 타입은 'Direction' 타입에 할당될 수 없습니다.
// 2. 숫자 리터럴 타입
type StatusCode = 200 | 404 | 500;
let status: StatusCode = 200;
// status = 201; // 🚨 오류
// 3. boolean 리터럴 타입 (사용 빈도는 낮음)
let isTrue: true = true;
// isTrue = false; // 🚨 오류
// 리터럴 타입을 함수 매개변수에 사용하면 인자로 허용되는 값이 제한됩니다.
function setDirection(dir: Direction): void {
console.log(`Setting direction to: ${dir}`);
}
setDirection("left");
2. 함수 (Functions)
🔷 1. 함수 선언과 타입
// 1. 함수 선언식 (Function Declaration)
// 매개변수 a와 b는 number 타입, 반환 값은 number 타입임을 명시
function add(a: number, b: number): number {
return a + b;
}
// 2. 함수 표현식 (Function Expression)
// 변수 자체에 함수 타입 시그니처를 지정할 수도 있습니다.
type Calculator = (x: number, y: number) => number;
const subtract: Calculator = function (a, b) {
// 반환 값이 number 타입 시그니처와 일치하지 않으면 오류 발생
return a - b;
};
// 3. 반환 타입 생략 (타입 추론)
// TypeScript는 return 값을 보고 반환 타입을 자동으로 추론합니다 (여기서는 string).
function createMessage(text: string) {
return `Message: ${text}`;
}
// 명시적으로 void 타입을 사용하여 반환 값이 없음을 나타냅니다.
function logResult(result: number): void {
console.log(`The result is: ${result}`);
// return "Error"; // 🚨 오류: void 타입은 값을 반환할 수 없습니다.
}
logResult(add(5, 3)); // 8이 콘솔에 출력되고, 반환 값은 없음
🔷 2. 화살표 함수 (Arrow Functions)
▸ 기본 구조 및 문법 간소화
// 기본 형태 (명시적 반환)
const add = (a: number, b: number): number => {
return a + b;
};
// 암시적 반환 (가장 흔한 형태)
const subtract = (a: number, b: number): number => a - b;
console.log(subtract(10, 4)); // 출력: 6
// 괄호 사용
const logValueWithBrackets = (value: string): void => {
console.log(`Value: ${value}`);
};
// 괄호 생략
const logValue = (value: string): void => console.log(value);
logValue("Hello, TypeScript"); // 출력: Hello, TypeScript
// 매개변수 없이 현재 시간을 문자열로 반환
const getTimeString = (): string => new Date().toLocaleTimeString();
console.log(`Current Time: ${getTimeString()}`);
//객체 리터럴 반환 시
type Coordinates = { x: number, y: number };
// 못된 사용 (함수 본문으로 해석되어 undefined 반환)
// const createCoordBroken = (x: number, y: number) => { x: x, y: y };
// 올바른 사용: 객체 리터럴을 괄호로 감쌈
const createCoord = (x: number, y: number): Coordinates => ({ x: x, y: y });
// 속성 이름 축약 (Shorthand) 사용
const createPoint = (x: number, y: number): Coordinates => ({ x, y });
console.log(createPoint(1, 5)); // 출력: { x: 1, y: 5 }
▸ 고차 함수 (Higher-Order Functions) 콜백
const numbers = [1, 2, 3, 4, 5];
// 2.1. filter: 짝수만 걸러내기
const evens: number[] = numbers.filter(n => n % 2 === 0);
console.log(`Evens: ${evens}`); // 출력: [2, 4]
// 2.2. map: 각 항목을 제곱하여 새로운 배열 생성
const squares: number[] = numbers.map(n => n * n);
console.log(`Squares: ${squares}`); // 출력: [1, 4, 9, 16, 25]
// 2.3. reduce: 배열의 모든 값을 합산
const sum: number = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(`Sum: ${sum}`); // 출력: 15
🔷 3. 선택적 매개변수와 기본값
// lastName 매개변수는 선택적입니다.
// lastName은 string 또는 undefined 타입으로 추론됩니다.
function greet(firstName: string, lastName?: string): string {
if (lastName) {
return `Hello, ${firstName} ${lastName}!`;
}
return `Hello, ${firstName}!`;
}
console.log(greet("Minho")); // 출력: Hello, Minho!
console.log(greet("Minho", "Kim")); // 출력: Hello, Minho Kim!
// count에 기본값 10을 설정
function getArray(item: string, count: number = 10): string[] {
return new Array(count).fill(item);
}
// count를 생략하면 기본값 10이 사용됨
console.log(getArray("Default", 5).length); // 출력: 5
console.log(getArray("Default").length); // 출력: 10
🔷 4. Rest 파라미터 (나머지 매개변수)
// items: string[] 타입으로 추론됩니다.
function concatenateStrings(separator: string, ...items: string[]): string {
// items는 항상 string 배열입니다.
console.log(`Items count: ${items.length}`);
return items.join(separator);
}
const names = concatenateStrings(", ", "Alice", "Bob", "Charlie", "David");
// 출력: Items count: 4
// 반환: Alice, Bob, Charlie, David
// 숫자 타입의 Rest 파라미터 예시
function sumAll(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sumAll(1, 2, 3, 4, 5)); // 출력: 15
🔷 5. 함수 오버로딩 (Function Overloading) : 함수 시그니처 정의
// 1. 오버로드 시그니처 (호출 방식만 정의)
// string 또는 number를 받아 string을 반환
function convert(input: string): string;
function convert(input: number): number;
// Date 타입을 받아 string을 반환
function convert(input: Date): string;
// 2. 구현 시그니처 (실제 로직을 담는 함수 본문)
// 구현 시그니처는 가장 포괄적인 유니온 타입을 사용해야 합니다.
function convert(input: string | number | Date): string | number {
if (typeof input === 'string') {
return input.toUpperCase(); // string -> string
} else if (typeof input === 'number') {
return input * 2; // number -> number
} else if (input instanceof Date) {
return input.toISOString(); // Date -> string
}
// never 타입으로 떨어지는 예외 처리 (선택적)
throw new Error("Unsupported input type.");
}
// 타입 안전성 검증:
const resultStr = convert("hello"); // resultStr은 string 타입으로 추론
const resultNum = convert(100); // resultNum은 number 타입으로 추론
const resultDate = convert(new Date()); // resultDate는 string 타입으로 추론
// const resultInvalid = convert(true); // 🚨 오류: 오버로드 시그니처에 true를 받는 정의가 없음
🔷 6. 제네릭 함수 (Generic Functions)
// <T>는 타입 변수(Type Variable)입니다.
// T는 함수가 호출될 때 전달된 인수의 타입으로 결정됩니다.
function identity<T>(arg: T): T {
// T가 어떤 타입이든, 입력 타입과 출력 타입이 동일함을 보장합니다.
return arg;
}
// 1. 명시적 타입 지정 (선택적)
let output1 = identity<string>("my string"); // output1은 string 타입
let output2 = identity<number>(12345); // output2는 number 타입
// 2. 타입 추론 (가장 일반적인 사용법)
// TypeScript가 인수를 보고 자동으로 타입을 추론합니다.
let output3 = identity(true); // output3은 boolean 타입
let output4 = identity([1, 2]); // output4는 number[] 타입
// 제네릭과 유니온 타입 활용 예시
// 두 값을 교환하는 함수: 입력 A, B의 타입이 다를 수 있으며, 반환되는 튜플의 타입은 [B, A]입니다.
function swapValues<A, B>(a: A, b: B): [B, A] {
return [b, a];
}
const swapped = swapValues(10, "Text"); // swapped는 [string, number] 타입
console.log(swapped); // 출력: ["Text", 10]
3. 클래스와 객체지향 프로그래밍 (Classes and OOP)
🔷 1. 기본 문법, 접근제어자(Access Modifiers), 속성 접근자(get, set)
// Employee 클래스 정의
class Employee {
// 1. 클래스 필드 정의 및 접근 제어자 활용
// public: 어디서든 접근 가능 (기본값, 명시적으로 붙여줌)
public name: string;
// private: 오직 이 클래스 내부에서만 접근 가능
private _salary: number;
// protected: 이 클래스와 상속받는 자식 클래스 내부에서만 접근 가능
protected hireYear: number;
// readonly: 초기화 후 수정 불가 (급여 등급은 변하지 않음)
readonly salaryGrade: "A" | "B" | "C";
// 2. 생성자 (Constructor)
// 생성자 매개변수에 접근 제어자를 붙여 필드 정의와 초기화를 동시에 수행할 수 있습니다.
constructor(
name: string,
initialSalary: number,
hireYear: number,
grade: "A" | "B" | "C" = "B"
) {
this.name = name;
this._salary = initialSalary;
this.hireYear = hireYear;
this.salaryGrade = grade;
}
// 3. 속성 접근자 (Getter/Setter)
// 3.1. Getter: _salary 값을 읽을 때 호출
// 외부에서는 'employee.salary'처럼 속성처럼 접근합니다.
get salary(): number {
// 급여를 반환하기 전에 계산된 보너스 정보를 포함할 수 있습니다.
const bonus = this.getBonusRate();
return this._salary * (1 + bonus);
}
// 3.2. Setter: _salary 값을 할당할 때 호출
// 외부에서는 'employee.salary = 60000'처럼 속성처럼 할당합니다.
set salary(newSalary: number) {
// 유효성 검사: 급여가 음수가 되는 것을 방지
if (newSalary < 0) {
console.error("🚨 오류: 급여는 음수가 될 수 없습니다.");
return;
}
this._salary = newSalary;
}
// 3.3. Getter (계산된 속성): 서비스 연한
// private 속성인 hireYear를 사용하여 현재 연도 기준으로 서비스 연한을 계산합니다.
get yearsOfService(): number {
const currentYear = new Date().getFullYear();
// protected 속성인 this.hireYear에 접근 가능
return currentYear - this.hireYear;
}
// private 메서드: 오직 내부 로직에만 사용
private getBonusRate(): number {
if (this.salaryGrade === "A") {
return 0.15; // 15% 보너스
} else if (this.salaryGrade === "B" && this.yearsOfService >= 5) {
return 0.10; // 10% 보너스
}
return 0; // 보너스 없음
}
// public 메서드
public introduce(): string {
return `${this.name} (${this.salaryGrade} Grade). Service: ${this.yearsOfService} years.`;
}
}
// ----------------------------------------------------
// 클래스 사용 예시
// ----------------------------------------------------
// 2018년 입사, 급여 50000, A등급의 직원 생성 (2025년 기준 7년차)
const manager = new Employee("Olivia", 50000, 2018, "A");
console.log(manager.introduce());
// 출력: Olivia (A Grade). Service: 7 years.
// 1. public 속성 접근
console.log(`Employee Name: ${manager.name}`); // 출력: Olivia
// 2. Getter 호출 (계산된 값 반환)
// A 등급 (15% 보너스): 50000 * 1.15 = 57500
console.log(`Annual Salary (with bonus): $${manager.salary}`); // 출력: $57500
// 3. Setter 호출 (유효성 검사 적용)
manager.salary = 60000; // 새로운 급여 설정 (Setter 호출)
console.log(`Updated Salary: $${manager.salary}`); // 출력: $69000 (60000 * 1.15)
// 4. private 속성 접근 시도
// console.log(manager._salary); // 🚨 오류: '_salary'는 private입니다.
// 5. protected 속성 접근 시도
// console.log(manager.hireYear); // 🚨 오류: 'hireYear'는 protected입니다.
// 6. readonly 속성 수정 시도
// manager.salaryGrade = "C"; // 🚨 오류: 읽기 전용 속성입니다.
🔷 2. 상속: 클래스 확장 및 super 키워드
// Account 클래스를 상속받는 SavingsAccount 클래스
class SavingsAccount extends Account {
private interestRate: number;
constructor(name: string, initialBalance: number, accNum: string, rate: number) {
// 1. super() 호출: 부모 클래스(Account)의 생성자를 호출하여 부모 속성을 초기화해야 합니다.
super(name, initialBalance, accNum);
this.interestRate = rate;
}
// 부모 클래스의 메서드 재정의 (오버라이딩)
public introduce(): string {
// 2. super 키워드를 사용하여 부모 클래스의 메서드/속성에 접근할 수 있습니다.
const parentIntro = super.getMaskedNumber(); // 부모의 protected 속성을 사용
return `Savings account for ${this.ownerName}. Rate: ${this.interestRate}%. Acc: ${parentIntro}`;
}
public calculateInterest(): void {
// protected 속성은 자식 클래스 내부에서 접근 가능
// console.log(`Processing interest for ${this.accountNumber}`); // 접근 가능
}
}
const savings = new SavingsAccount("Brian", 5000, "0987654321", 0.05);
console.log(savings.introduce());
// 출력: Savings account for Brian. Rate: 0.05%. Acc: XXXX-4321
// savings.deposit(50); // 부모 클래스의 public 메서드 사용 가능
// console.log(savings.accountNumber); // 🚨 오류 (protected)
🔷 3. 추상 클래스 (Abstract Classes)
// 'abstract' 키워드로 정의
abstract class Shape {
// 공통 속성 정의
abstract name: string; // 추상 속성: 자식 클래스에서 반드시 구현해야 함
// 공통 메서드 (일반 메서드도 가질 수 있음)
getInfo(): string {
return `This is a ${this.name} shape.`;
}
// 추상 메서드: 본문이 없고, 자식 클래스에서 반드시 오버라이딩(구현)해야 합니다.
abstract calculateArea(): number;
}
// const shape = new Shape(); // 🚨 오류: 추상 클래스는 인스턴스화할 수 없습니다.
class Circle extends Shape {
radius: number;
name: string = "Circle"; // 추상 속성 구현
constructor(radius: number) {
super();
this.radius = radius;
}
// 추상 메서드 구현 강제
calculateArea(): number {
// Math.PI * r^2
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
width: number;
height: number;
name: string = "Rectangle"; // 추상 속성 구현
constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
calculateArea(): number {
return this.width * this.height;
}
}
const myCircle: Shape = new Circle(5);
const myRectangle: Shape = new Rectangle(4, 6);
console.log(myCircle.getInfo()); // 출력: This is a Circle shape.
console.log(myCircle.calculateArea()); // 출력: 78.53...
🔷 4. 인터페이스 구현 (Interface Implementation)
// 1. 인터페이스 정의 (계약서)
interface Logger {
log(message: string): void;
error(message: string): void;
level: 'info' | 'warn' | 'error';
}
// 2. 클래스에서 인터페이스 구현 (계약 이행)
class ConsoleLogger implements Logger {
level: 'info' | 'warn' | 'error' = 'info'; // 속성 구현
log(message: string): void { // 메서드 구현
if (this.level === 'info') {
console.log(`[INFO]: ${message}`);
}
}
error(message: string): void { // 메서드 구현
console.error(`[ERROR]: ${message}`);
}
// 인터페이스에 없는 메서드도 추가 가능
setLevel(newLevel: 'info' | 'warn' | 'error') {
this.level = newLevel;
}
}
// 타입 계약에 따라 Logger 타입으로 활용 가능
const myLogger: Logger = new ConsoleLogger();
myLogger.log("Application started.");
myLogger.error("A critical error occurred.");
// myLogger.setLevel('warn'); // 🚨 오류: Logger 인터페이스에 setLevel 메서드가 정의되어 있지 않음
4. 제네릭 (Generics) : 타입 변수
function identity<T>(arg: T): T {
// T는 입력 인수의 타입이며,
// 여기서 T는 string, number, boolean 등 어떤 타입이든 될 수 있습니다.
return arg;
}
🔷 1. 제네릭 함수: 다양한 타입에서 동작하는 함수 정의
// 예시 1: 배열의 첫 번째 요소 반환
// <T>를 사용하여, 입력 배열의 요소 타입 T를 그대로 반환 타입으로 사용합니다.
function getFirstElement<T>(arr: T[]): T {
if (arr.length === 0) {
// 배열이 비어있는 경우, T 타입으로 간주되는 undefined를 반환합니다.
// strictNullChecks가 true일 경우 T | undefined를 반환하는 것이 더 안전합니다.
// 여기서는 예시의 간결함을 위해 T를 사용합니다.
// 실제 코드에서는 T | undefined를 사용하는 것이 좋습니다.
}
return arr[0];
}
const numList = [10, 20, 30];
const firstNum = getFirstElement(numList); // firstNum은 number 타입
const strList = ["A", "B", "C"];
const firstStr = getFirstElement(strList); // firstStr은 string 타입
// firstNum.toUpperCase(); // 🚨 오류: number 타입에는 toUpperCase가 없습니다. (타입 안정성 보장)
// 예시 2: 여러 타입 변수 사용
// <T, U> 두 개의 타입 변수를 사용하여 서로 다른 타입의 두 인수를 받습니다.
function createPair<T, U>(v1: T, v2: U): [T, U] {
// 반환 타입은 첫 번째는 T, 두 번째는 U인 튜플입니다.
return [v1, v2];
}
// v1: string ("Key"), v2: number (123)
const mixedPair = createPair("Key", 123);
// mixedPair의 타입은 [string, number]
// v1: boolean (true), v2: string ("Status")
const statusPair = createPair(true, "Status");
// statusPair의 타입은 [boolean, string]
console.log(`Key: ${mixedPair[0]}, Value: ${mixedPair[1]}`);
🔷 2. 제네릭 인터페이스: 데이터 구조의 타입 유연성 확보
// <T>는 이 인터페이스를 구현하거나 사용할 때 결정됩니다.
interface Box<T> {
value: T;
// T 타입의 값을 처리하는 메서드도 정의 가능
read(): T;
}
// Box 인터페이스를 string 타입으로 사용하여 구현
const stringBox: Box<string> = {
value: "Generics",
read: function() {
return this.value;
}
};
// Box 인터페이스를 number 타입으로 사용하여 구현
const numberBox: Box<number> = {
value: 42,
read: function() {
return this.value;
}
};
stringBox.read().toUpperCase(); // string 타입 메서드 사용 가능
// numberBox.read().toUpperCase(); // 🚨 오류: number 타입 메서드 사용 불가
🔷 3. 제네릭 클래스 : 데이터 구조의 타입 유연성 확보
// Queue 클래스는 어떤 타입 T의 항목이든 저장할 수 있습니다.
class Queue<T> {
private items: T[] = []; // 내부 저장소는 T 타입의 배열입니다.
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
// string 타입을 다루는 큐 인스턴스
const stringQueue = new Queue<string>();
stringQueue.enqueue("Task 1");
stringQueue.enqueue("Task 2");
// stringQueue.enqueue(123); // 🚨 오류: number는 string 타입에 할당될 수 없음
const dequeuedItem = stringQueue.dequeue(); // dequeuedItem은 string 타입
console.log(dequeuedItem!.toUpperCase());
// 객체 타입을 다루는 큐 인스턴스
type Task = { id: number; description: string };
const taskQueue = new Queue<Task>();
taskQueue.enqueue({ id: 1, description: "Fix bug" });
const nextTask = taskQueue.dequeue(); // nextTask는 Task 타입
console.log(nextTask!.description);
🔷 4. 제네릭 제약조건 (Constraints) : extends 키워드
제네릭 타입 T가 특정 속성을 가지고 있어야만 함수나 클래스 내부에서 해당 속성을 안전하게 사용할 수 있습니다
▸ 예시: length 속성 제약
// T extends { length: number }
// T는 반드시 length라는 number 타입의 속성을 가지고 있어야 합니다.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
// 이제 T는 length 속성을 가지고 있음이 보장되므로, 안전하게 접근 가능합니다.
console.log(arg.length);
return arg;
}
// 1. 제약조건을 만족하는 경우 (string, Array는 length를 가짐)
loggingIdentity("Hello"); // string은 length 속성을 가짐
loggingIdentity([1, 2, 3]); // Array는 length 속성을 가짐
// 2. 제약조건을 만족하지 않는 경우
// loggingIdentity(10); // 🚨 오류: number 타입에는 length 속성이 없습니다.
// 3. 커스텀 객체로 제약조건 만족
const myObject = { value: 5, length: 100 };
loggingIdentity(myObject); // 객체가 length를 가지므로 허용됨 (출력: 100)
▸ 예시: 객체 키에 대한 제약
객체의 속성 이름을 안전하게 가져오는 함수를 만들 때 사용됩니다
// <T>는 객체 타입, <K>는 T의 키 타입입니다.
// K extends keyof T: K는 반드시 T 객체가 가지는 속성 이름 중 하나여야 합니다.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
// key가 T에 존재함이 보장되므로 안전하게 접근 가능
return obj[key];
}
const userProfile = { id: 1, username: "dev_user", isActive: true };
// 'username'은 userProfile의 키이므로 허용
const userName = getProperty(userProfile, "username"); // userName은 string 타입
// 'age'는 userProfile의 키가 아니므로 컴파일 오류 발생
// 🚨 오류: 'age' 형식은 'keyof T' 형식에 할당될 수 없습니다.
// const userAge = getProperty(userProfile, "age");
5. 고급 타입 조작 (Advanced Type Manipulation)
🔷 1. 타입 가드 (Type Guards)와 타입 좁히기 : typeof, instanceof, is
타입 좁히기(Type Narrowing)는 유니온 타입처럼 여러 후보 타입을 가진 변수의 타입을 특정 코드 블록 내에서 하나의 타입으로 확정하는 과정입니다.
▸ typeof 및 instanceof 가드 : 원시 타입 및 클래스 인스턴스 검사에 사용됩니다.
// 유니온 타입 정의
type Data = string | Date;
function processData(data: Data): void {
if (typeof data === 'string') {
// typeof 가드: data는 string 타입으로 좁혀집니다.
console.log(`String length: ${data.length}`);
} else if (data instanceof Date) {
// instanceof 가드: data는 Date 타입으로 좁혀집니다.
console.log(`Year: ${data.getFullYear()}`);
} else {
// never 타입 영역 (모든 경우가 커버되지 않으면 오류)
}
}
processData("hello"); // 출력: String length: 5
processData(new Date()); // 출력: Year: 2025 (현재 연도)
▸ 사용자 정의 타입 가드 (is) : 개발자가 직접 런타임 검사 함수를 만들어 타입을 좁히는 규칙을 정의합니다.
interface ApiResponse { success: true; data: string; }
interface ApiError { success: false; error: string; }
type Result = ApiResponse | ApiError;
// 반환 타입: result is ApiResponse -> true일 경우 result는 ApiResponse로 좁혀짐
function isSuccess(result: Result): result is ApiResponse {
return result.success === true;
}
function handleResult(result: Result) {
if (isSuccess(result)) {
// isSuccess가 true이므로 result는 ApiResponse 타입입니다.
console.log(`Success data: ${result.data.toUpperCase()}`);
} else {
// result는 ApiError 타입입니다.
console.error(`Error details: ${result.error}`);
}
}
handleResult({ success: false, error: "Not Found" });
🔷 2. 타입 단언 (as Type)과 안정성 개선 (satisfies)
▸ 타입 단언 (Type Assertions, as Type)
const apiData: unknown = { id: 101, username: "tester" };
// 컴파일러에게 apiData가 User 객체 형태임을 강제로 확신시킵니다.
interface User { id: number; username: string; }
const user = apiData as User;
// 실제 런타임 객체 구조와 단언 타입이 다를 경우 런타임 오류가 발생할 수 있습니다.
console.log(user.username); // 안전한 접근
▸ 타입 안정성 및 추론 개선: satisfies 키워드
type AllowedKeys = 'config' | 'style';
type SettingValues = Record<AllowedKeys, string>;
// 🚨 문제: 'as'를 사용하면 "config"의 구체적인 타입 ("large")을 잃고 string 타입으로 추론됩니다.
const settingsAs = {
config: "large",
style: "dark"
} as SettingValues;
// settingsAs.config의 타입은 string. 리터럴 메소드 (slice 등) 사용 시 오류 방지 불가.
// ✅ 해결책: satisfies 키워드
// SettingValues를 만족하는지 검사하면서, 각 속성의 리터럴 타입을 보존합니다.
const settingsSatisfies = {
config: "large",
style: "dark"
} satisfies SettingValues;
// settingsSatisfies.config의 타입은 리터럴 타입 "large"입니다.
// 리터럴 타입 메서드를 안전하게 사용 가능 (출력: lar)
console.log(settingsSatisfies.config.slice(0, 3));
// 🚨 satisfies는 타입 안정성도 보장합니다.
/*
const invalidSettings = {
// 🚨 오류: 'unknownKey'는 AllowedKeys 타입에 속하지 않아 컴파일 오류 발생
unknownKey: "value"
} satisfies SettingValues;
*/
🔷 3. 조건부 타입 (Conditional Types)
타입 시스템 내에서 삼항 연산자(T extends U ? X : Y)를 사용하여 타입을 조건적으로 결정할 수 있게 해 줍니다
// T가 number[]를 상속(확장)하는지 확인하고 타입을 결정합니다.
type IsNumberArray<T> = T extends number[] ? true : false;
type Res1 = IsNumberArray<number[]>; // Res1은 true
type Res2 = IsNumberArray<string[]>; // Res2은 false
// T가 유니온 타입일 경우, 각 멤버에 대해 조건이 개별적으로 적용됩니다. (분배 법칙)
type NonString<T> = T extends string ? never : T;
type Mixed = NonString<string | number | boolean>; // Mixed는 number | boolean
// (string -> never) | (number -> number) | (boolean -> boolean)
▸ infer 키워드의 활용
infer 키워드는 조건부 타입 내에서 타입 변수를 추론하거나 추출할 수 있게 해 줍니다.
// GetElementType<T>는 T가 배열 타입인 경우, 배열의 요소 타입(E)을 추론하여 반환합니다.
type GetElementType<T> = T extends Array<infer E> ? E : T;
type ItemType1 = GetElementType<string[]>; // ItemType1은 string
type ItemType2 = GetElementType<number>; // ItemType2은 number (배열이 아니므로 T 반환)
// Promise의 해결된 타입을 추출하는 Awaited<T>의 핵심 원리
type ExtractPromiseResult<T> = T extends Promise<infer R> ? R : T;
type PromiseResult = ExtractPromiseResult<Promise<{ data: string }>>;
// PromiseResult는 { data: string } 타입
🔷 4. Mapped Types (매핑된 타입)
Mapped Types는 TypeScript의 문법적 기능 그 자체이며, 기존 타입의 속성들을 순회하면서 새로운 타입을 생성하는 변환 로직을 정의하는 데 사용됩니다.
type NewType<T> = {
[K in keyof T]: /* K와 T[K]를 활용한 새로운 타입 표현식 */;
};
//1. keyof T: 기존 타입 T의 모든 속성 이름(리터럴 유니온 타입)을 가져옵니다.
//2. K in ...: 이 속성 이름들(K)을 반복(순회)합니다.
//3. T[K]: 반복되는 현재 속성 이름(K)에 해당하는 기존 속성의 타입입니다.
interface APIResponse {
id: number;
name: string;
isActive: boolean;
}
// 1. Partial<T> 구현 원리: 모든 속성을 선택적(`?`)으로 변환
type Optional<T> = {
[K in keyof T]?: T[K]; // keyof T는 'id' | 'name' | 'isActive'
};
type PartialAPIResponse = Optional<APIResponse>;
/*
{
id?: number;
name?: string;
isActive?: boolean;
}
*/
// 2. Flags 타입: 모든 속성의 값을 boolean으로 변환
type Flags<T> = {
[K in keyof T]: boolean;
};
type APIFlags = Flags<APIResponse>;
/*
{
id: boolean;
name: boolean;
isActive: boolean;
}
*/
🔷 5. 유틸리티 타입 (Utility Types)
유틸리티 타입은 TypeScript 컴파일러에 내장되어 있거나 혹은 외부 라이브러리에서 미리 정의되어 제공되는 제네릭 타입 별칭입니다.
// --- 1. 기본 타입 정의 ---
interface UserProfile {
// 필수 속성
id: number;
username: string;
// 선택적 속성 (원본에 이미 ?가 있음)
email?: string;
// 읽기 전용 속성
readonly createdAt: Date;
// 함수 속성
// Promise를 반환하는 비동기 함수
fetchActivity: (userId: number) => Promise<string[]>;
}
type UserStatus = 'active' | 'pending' | 'deleted' | null | undefined;
type ValidId = string | number;
type MixedUnion = string | number | boolean | null | undefined;
// --- 2. 주요 유틸리티 타입 적용 예시 ---
console.log("--- 2.1. 속성 조작 유틸리티 (Property Manipulation) ---");
// 2.1.1. Partial<T>
// 모든 속성을 선택적(? 없어도 되게)으로 만듭니다.
type PartialUser = Partial<UserProfile>;
const updateProfile: PartialUser = {
// id, username이 필수가 아니게 됩니다.
username: "new_name",
email: "new@example.com"
};
console.log("// Partial<T>: 모든 속성을 선택적으로 만듦");
// 2.1.2. Readonly<T>
// 모든 속성을 읽기 전용(readonly)으로 만듭니다.
type ReadonlyUser = Readonly<UserProfile>;
const immutableUser: ReadonlyUser = {
id: 1, username: "read_only", createdAt: new Date(),
fetchActivity: () => Promise.resolve([])
};
// immutableUser.username = "new"; // 🚨 오류: 읽기 전용 속성입니다.
console.log("// Readonly<T>: 모든 속성을 읽기 전용으로 만듦");
// 2.1.3. Pick<T, K>
// T 타입에서 K에 지정된 속성들만 선택합니다.
type UserSummary = Pick<UserProfile, 'id' | 'username'>;
const summary: UserSummary = { id: 2, username: "summary_user" };
console.log("// Pick<T, K>: 지정된 속성만 선택");
// 2.1.4. Omit<T, K>
// T 타입에서 K에 지정된 속성들을 제외합니다.
type UserWithoutSensitive = Omit<UserProfile, 'id' | 'createdAt'>;
const safeUser: UserWithoutSensitive = {
username: "safe_user",
email: "safe@mail.com", // email은 원본에서 선택적이었으므로 그대로 선택적
fetchActivity: () => Promise.resolve([])
};
console.log("// Omit<T, K>: 지정된 속성만 제외");
console.log("\n--- 2.2. 유니온 타입 조작 유틸리티 (Union Manipulation) ---");
// 2.2.1. Exclude<T, U>
// T 유니온 타입에서 U와 호환되는 타입(또는 멤버)을 제외합니다.
type OnlyStringOrNumber = Exclude<MixedUnion, boolean | null | undefined>;
// OnlyStringOrNumber는 string | number 타입
console.log(`// Exclude<T, U>: ${'A'|'B'|'C'|'D'}에서 ${'A'|'C'}를 제외 -> ${'B'|'D'}`);
// 2.2.2. Extract<T, U>
// T 유니온 타입에서 U와 호환되는 타입(또는 멤버)만 추출합니다.
type OnlyNullish = Extract<UserStatus, null | undefined>;
// OnlyNullish는 null | undefined 타입
console.log(`// Extract<T, U>: ${'A'|'B'|'C'|'D'}에서 ${'A'|'C'}만 추출 -> ${'A'|'C'}`);
// 2.2.3. NonNullable<T>
// T 유니온 타입에서 null과 undefined를 제거합니다.
type DefinedStatus = NonNullable<UserStatus>;
// DefinedStatus는 'active' | 'pending' | 'deleted' 타입
console.log("// NonNullable<T>: null과 undefined를 제거");
console.log("\n--- 2.3. 함수 및 비동기 타입 조작 유틸리티 ---");
// 2.3.1. Parameters<T>
// 함수 타입 T의 매개변수 타입을 튜플로 추출합니다.
type FetchParams = Parameters<UserProfile['fetchActivity']>;
// FetchParams는 [userId: number] 타입
const userIdTuple: FetchParams = [5];
console.log("// Parameters<T>: 함수의 매개변수를 튜플로 추출");
// 2.3.2. ReturnType<T>
// 함수 타입 T의 반환 타입을 추출합니다. (Promise<T> 자체를 반환)
type FetchReturn = ReturnType<UserProfile['fetchActivity']>;
// FetchReturn은 Promise<string[]> 타입
console.log("// ReturnType<T>: 함수의 반환 타입 추출 (Promise 포함)");
// 2.3.3. Awaited<T> (Node.js 비동기 컨텍스트에서 중요)
// Promise T가 해결된 후의 최종 결과 타입(Promise 내부 타입)을 추출합니다.
type ActivityArray = Awaited<FetchReturn>;
// ActivityArray는 string[] 타입 (Promise가 제거됨)
console.log("// Awaited<T>: Promise가 해결된 후의 타입 추출");
console.log("\n--- 2.4. 기타 유틸리티 ---");
// 2.4.1. Record<K, T>
// K 타입의 키와 T 타입의 값으로 구성된 객체 타입을 생성합니다.
type RolePermissions = 'admin' | 'user' | 'guest';
type PermissionsMap = Record<RolePermissions, boolean>;
const permissions: PermissionsMap = {
admin: true,
user: true,
guest: false
};
console.log("// Record<K, T>: 특정 키-값 쌍을 가진 객체 타입 생성");
6. 비동기 프로그래밍 (Asynchronous Programming)
🔷 1. Promise와 타입: Promise<T>의 타입 정의 및 활용
Promise는 비동기 작업의 최종 성공 값(결과) 또는 실패 이유(오류)를 나타내는 객체입니다.
// Promise<T> 정의: T는 성공 시 반환될 데이터의 타입입니다.
type FetchResult = { data: string };
function fetchData(id: number): Promise<FetchResult> {
return new Promise((resolve, reject) => {
// 실제 비동기 I/O 작업 (예: 네트워크 요청) 시뮬레이션
setTimeout(() => {
if (id > 0) {
// resolve 함수는 성공 시 반환될 FetchResult 타입의 객체를 인수로 받습니다.
resolve({ data: `User data for ID ${id}` });
} else {
// reject 함수는 실패 이유(Error 객체 또는 string 등)를 인수로 받습니다.
reject(new Error("Invalid ID provided."));
}
}, 100);
});
}
// Promise 사용 (then/catch 체이닝)
fetchData(5)
.then(result => {
// result는 FetchResult 타입으로 추론됩니다.
console.log(`Success: ${result.data}`);
// Promise 체이닝: 다음 .then()으로 string 타입의 새 Promise를 반환
return result.data.toUpperCase();
})
.then(upperData => {
// upperData는 string 타입으로 추론됩니다.
console.log(`Uppercase: ${upperData}`);
})
.catch(error => {
// error는 Error 타입으로 추론됩니다.
console.error(`Error: ${error.message}`);
});
🔷 2. async/await: 비동기 코드의 동기적 표현 및 타입 유추
async/await는 Promise 기반 코드를 마치 동기 코드처럼 읽기 쉽고 간결하게 작성할 수 있도록 하는 문법적 설탕(Syntactic Sugar)입니다.
▸ async 함수: 항상 Promise를 반환하도록 함수를 선언합니다. 함수의 반환 타입 T는 자동으로 Promise<T>가 됩니다.
▸ await 키워드: async 함수 내에서만 사용 가능하며, Promise가 해결(resolve)될 때까지 함수의 실행을 일시 중지하고, Promise의 최종 결과 값을 반환합니다.
// async 함수는 반환 타입이 명시되지 않아도 Promise<{ data: string }>로 추론됩니다.
async function processUserData(id: number): Promise<string> {
try {
// 1. await: fetchData Promise가 완료될 때까지 대기합니다.
// result는 Promise<FetchResult>가 아닌, 해결된 타입인 FetchResult 타입입니다.
const result = await fetchData(id);
// 2. 다른 비동기 함수 호출 (예시를 위해 단순화)
const additionalInfo = await simulateDelay(50);
// 3. 최종적으로 string 값을 반환하지만, async 함수이므로 Promise<string>으로 래핑됩니다.
return `${result.data}. Info: ${additionalInfo}`;
} catch (error) {
// 4. 에러 발생 시 try...catch로 잡을 수 있으며, Promise는 reject 상태가 됩니다.
// Node.js 환경에서 error는 기본적으로 unknown 타입으로 추론될 수 있습니다.
if (error instanceof Error) {
throw new Error(`Processing failed: ${error.message}`);
}
throw new Error("An unknown error occurred.");
}
}
// Promise<T>를 반환하는 Helper 함수
function simulateDelay(ms: number): Promise<string> {
return new Promise(resolve => setTimeout(() => resolve("Loaded"), ms));
}
// 호출 및 결과 확인
processUserData(10)
.then(finalString => {
// finalString은 string 타입입니다.
console.log(`\nFinal Output: ${finalString}`);
})
.catch(err => {
console.error(`\nError caught by caller: ${err.message}`);
});
processUserData(-1) // ID가 음수이므로 에러 발생
.catch(err => {
console.error(`\nError caught for invalid ID: ${err.message}`);
});
🔷 3. 비동기 에러 핸들링: try...catch 및 .catch()의 적절한 사용
비동기 에러는 Promise의 reject 상태로 전달됩니다. 이를 포착하고 처리하는 방법은 async/await를 사용하느냐 then() 체이닝을 사용하느냐에 따라 달라집니다.
▸ async/await 에러 핸들링
async function safeApiCall(id: number) {
try {
const result = await fetchData(id); // Promise가 reject되면 여기서 catch로 점프
console.log(`Success: ${result.data}`);
} catch (error) {
// TypeScript 4.4+부터 catch 블록의 변수(error)는 기본적으로 unknown 타입입니다.
// 따라서 에러 객체의 속성(message 등)에 접근하려면 반드시 타입 가드가 필요합니다.
if (error instanceof Error) {
console.error(`[HANDLED ERROR]: ${error.message}`);
} else {
console.error("[UNKNOWN ERROR TYPE]");
}
// 에러를 재전파하지 않으면 Promise는 resolve 상태가 됩니다.
// 재전파하려면 `throw error;`를 사용해야 합니다.
}
}
safeApiCall(-5); // 에러 발생 및 [HANDLED ERROR] 출력
▸ Promise 체이닝 에러 핸들링
fetchData(0) // ID가 0이므로 reject 발생 가정
.then(result => {
console.log(`Should not reach here: ${result.data}`);
})
.catch(error => {
// 체인의 어느 단계에서든 발생한 reject를 여기서 포착합니다.
if (error instanceof Error) {
console.error(`[CHAIN CATCH]: ${error.message}`);
}
})
// 최종적으로 .finally()를 사용하여 성공 또는 실패에 관계없이 실행할 코드를 정의할 수 있습니다.
.finally(() => {
console.log("Async operation finished.");
});
🔷 4. Promise 유틸리티: Promise.all(), Promise.race(), Promise.allSettled()
여러 개의 비동기 작업을 동시에 실행하고 결과를 취합할 때 사용됩니다.
▸ Promise.all(iterable)
모든 Promise가 성공해야 성공합니다. 하나라도 실패하면 전체가 실패합니다.
// 타입: Promise<[string, number]> (인수의 순서와 타입이 유지됨)
async function runAll() {
const promise1 = Promise.resolve("Hello");
const promise2 = Promise.resolve(42);
const promise3 = fetchData(1);
try {
// results의 타입은 Awaited된 타입들의 튜플 [string, number, FetchResult]
const results = await Promise.all([promise1, promise2, promise3]);
// 타입 안전하게 접근 가능
console.log("\n--- Promise.all() Success ---");
console.log(`Type of 1st: ${typeof results[0]}`);
console.log(`Type of 3rd data: ${results[2].data}`);
} catch (error) {
// 하나라도 reject되면 이 블록이 실행됩니다.
console.error("Promise.all() failed:", error);
}
}
runAll();
▸ Promise.race(iterable)
가장 먼저 완료(성공 또는 실패)되는 Promise의 결과를 반환합니다.
async function runRace() {
const slow = new Promise<string>(resolve => setTimeout(() => resolve("Slow done"), 500));
const fast = new Promise<string>(resolve => setTimeout(() => resolve("Fast done"), 100));
// result는 string 타입으로 추론됩니다.
const result = await Promise.race([slow, fast]);
console.log("\n--- Promise.race() ---");
console.log(`Winner: ${result}`); // 출력: Fast done
}
runRace();
▸ Promise.allSettled(iterable)
모든 Promise가 완료(성공 또는 실패 무관)될 때까지 기다리며, 각 Promise의 결과 상태를 포함하는 객체의 배열을 반환합니다.
// Promise.allSettled의 반환 타입은 Promise<Array<PromiseSettledResult<T>>> 입니다.
async function runAllSettled() {
const successPromise = Promise.resolve(100);
const failurePromise = Promise.reject(new Error("Network fail"));
// results 타입: Array<{ status: 'fulfilled', value: number } | { status: 'rejected', reason: Error }>
const results = await Promise.allSettled([successPromise, failurePromise]);
console.log("\n--- Promise.allSettled() ---");
results.forEach(result => {
if (result.status === 'fulfilled') {
// result.value는 number 타입입니다.
console.log(`Fulfilled with value: ${result.value}`);
} else {
// result.reason은 unknown 타입이며, 타입 가드로 Error 객체임을 확인합니다.
console.error(`Rejected with reason: ${result.reason instanceof Error
? result.reason.message : result.reason}`);
}
});
}
runAllSettled();
7. 모듈 시스템 (ESM for Node.js) : export, import
🔷 Named Export (이름 지정 내보내기)
여러 기능을 내보낼 때 사용되며, 가져올 때도 동일한 이름을 사용해야 합니다.
// --- math.ts ---
// 1. 함수와 변수를 선언과 동시에 내보내기
export const PI: number = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
// 2. 선언 후 별도로 내보내기
function subtract(a: number, b: number): number {
return a - b;
}
export { subtract };
// --- app.ts ---
// 1. 필요한 항목만 구조 분해 할당 형태로 가져오기
import { PI, add, subtract } from './math.js';
// Note: Node.js ESM은 .js 또는 .mjs 확장자를 명시해야 합니다.
// (TS 파일도 컴파일된 JS 파일을 가리킵니다.)
console.log(add(10, 5)); // 출력: 15
console.log(subtract(10, 5)); // 출력: 5
// 2. 전체 모듈을 하나의 객체로 가져오기 (Namespace Import)
import * as MathUtils from './math.js';
console.log(MathUtils.PI); // 출력: 3.14159
🔷 Default Export (기본 내보내기)
모듈당 하나의 주요 항목만 내보낼 때 사용되며, 가져올 때 {} 없이 원하는 이름으로 자유롭게 가져올 수 있습니다.
// --- user.ts ---
interface User {
name: string;
id: number;
}
class UserService {
getUser(id: number): User {
return { name: `User_${id}`, id };
}
}
// 모듈당 하나만 허용
export default UserService;
// --- app.ts ---
// 가져올 때 {} 없이 원하는 이름(UserService는 MyService로)으로 가져옵니다.
import MyService from './user.js';
const service = new MyService();
const user = service.getUser(10);
console.log(`Default Export User: ${user.name}`); // 출력: Default Export User: User_10
🔷 동적 Import: import()
동적 import()는 런타임에 필요할 때만 모듈을 비동기적으로 로드할 수 있게 해 줍니다.
이는 코드 스플리팅(Code Splitting)이나 조건부 로딩에 유용합니다. import()는 Promise를 반환합니다.
// 조건에 따라 모듈을 비동기로 로드
async function loadHeavyModule(condition: boolean) {
if (condition) {
// import('./heavy-calc.js')는
// Promise<{ add: Function, default: Class }> 형태를 반환합니다.
const module = await import('./heavy-calc.js');
// Named Export 접근
const result = module.add(5, 5);
console.log(`Dynamic Load Result: ${result}`);
// Default Export 접근
const HeavyService = module.default;
// ...
}
}
// 모듈 로드 함수 호출
loadHeavyModule(true);
🔷 타입 전용 Import: import type ...
TypeScript에서만 사용되는 구문으로, 가져오는 내용이 타입에만 관련되어 런타임에 JavaScript 코드로 컴파일되지 않도록 보장합니다.
이는 모듈이 실제로 존재하지 않아도 되므로 빌드 성능과 안전성을 향상시킵니다.
// --- types/config.ts ---
export interface Config {
timeout: number;
retries: number;
}
// --- main.ts ---
// 1. 타입만 가져오는 경우 (런타임 코드 생성 X)
// Config는 타입 정의로만 사용됩니다.
import type { Config } from './types/config.js';
// 2. 값과 타입을 동시에 가져오는 경우
// import { SomeValue, type SomeType } from './some-module.js';
class App {
private settings: Config; // Config는 타입으로만 사용
constructor(config: Config) {
this.settings = config;
}
}
// 런타임에 사용되는 값은 일반 import를 사용합니다.
import { fetchConfig } from './config-loader.js';
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.