함수와 고급 타입 시스템 (Functions & Advanced Type System)
TypeScript에서 **함수(Functions)**는 매개변수와 반환 타입을 명확하게 정의하여 코드의 안정성과 가독성을 높일 수 있다.
1️⃣ 함수 타입 정의 (Function Types)
함수의 매개변수와 반환값의 타입을 명시할 수 있다.
TypeScript는 반환 타입을 추론할 수 있지만, 명시적으로 지정하는 것이 권장된다.
기본적인 함수 타입 정의
function add(x: number, y: number): number {
return x + y;
}
console.log(add(5, 3)); // 8
x와 y는 number 타입을 받으며, 반환 값도 number로 지정되었다.
화살표 함수 타입 정의 ((x: number, y: number) => number)
const multiply = (x: number, y: number): number => x * y;
console.log(multiply(4, 2)); // 8
함수 타입 별칭 (type 사용)
type MathOperation = (x: number, y: number) => number;
const subtract: MathOperation = (x, y) => x - y;
console.log(subtract(10, 4)); // 6
type을 사용하면 함수의 타입을 재사용할 수 있어 유지보수가 쉬워진다.
2️⃣ 선택적 매개변수 (Optional Parameters ?)
어떤 매개변수가 필수가 아닐 경우, ?를 붙여 선택적 매개변수로 만들 수 있다.
function greet(name: string, age?: number): string {
return age ? `Hello, ${name}. You are ${age} years old.` : `Hello, ${name}.`;
}
console.log(greet("Alice")); // "Hello, Alice."
console.log(greet("Bob", 30)); // "Hello, Bob. You are 30 years old."
age 매개변수는 선택 사항이므로, 전달되지 않아도 오류가 발생하지 않는다.
⚠ 주의: 선택적 매개변수는 필수 매개변수 뒤에만 위치해야 한다.
function wrongFunc(age?: number, name: string): void {} // ❌ 오류 발생
3️⃣ 기본값 매개변수 (Default Parameters)
기본값을 제공하면, 인수가 생략되었을 때 기본값이 자동으로 적용된다.
function greet(name: string = "Guest"): string {
return `Hello, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"
console.log(greet("Alice"));// "Hello, Alice!"
name의 기본값은 "Guest"이며, 함수 호출 시 인수를 전달하지 않으면 "Guest"가 자동으로 사용된다.
4️⃣ Rest 매개변수 (...args)
Rest 매개변수는 개수에 제한이 없는 여러 개의 인수를 배열 형태로 받을 때 사용한다.
function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10, 20)); // 30
...numbers는 배열로 취급되며, 모든 값을 reduce()를 사용해 합산한다.
⚠ 주의:
Rest 매개변수는 함수의 마지막 매개변수로만 사용해야 한다.
function wrongFunc(x: number, ...args: number[], y: number): void {} // ❌ 오류 발생
유니온 타입과 교차 타입 (Union & Intersection)
TypeScript에서는 **유니온 타입(Union Type)**과 **교차 타입(Intersection Type)**을 사용하여 유연한 타입 정의가 가능하다.
유니온 타입 (Union Type)
여러 개의 타입 중 하나를 가질 수 있도록 하는 타입이다.
| (OR 연산자)를 사용하여 정의한다.
① 기본적인 유니온 타입
type Status = "success" | "error" | "pending";
let currentStatus: Status;
currentStatus = "success"; // ✅ 정상
currentStatus = "error"; // ✅ 정상
currentStatus = "pending"; // ✅ 정상
// currentStatus = "failed"; // ❌ 오류: "failed"는 Status 타입에 포함되지 않음
유니온 타입을 사용하면 값이 특정 문자열 집합 중 하나만 가능하도록 제한할 수 있다.
② 유니온 타입을 활용한 함수 매개변수
function printStatus(status: Status): void {
console.log(`현재 상태: ${status}`);
}
printStatus("success"); // ✅ 정상
printStatus("error"); // ✅ 정상
// printStatus("failed"); // ❌ 오류 발생
③ 유니온 타입을 포함한 여러 타입
type ID = number | string;
let userId: ID;
userId = 123; // ✅ 정상
userId = "abc"; // ✅ 정상
// userId = true; // ❌ 오류 발생 (number 또는 string만 가능)
유니온 타입을 사용하면 다양한 타입을 지원할 수 있어 유연성이 증가한다.
교차 타입 (Intersection Type)
여러 개의 타입을 조합하여 하나의 새로운 타입을 만드는 방식이다.
& (AND 연산자)를 사용하여 정의한다.
① 기본적인 교차 타입
type User = {
name: string;
age: number;
};
type Admin = {
role: string;
};
type Person = User & Admin;
const adminUser: Person = {
name: "Alice",
age: 30,
role: "admin",
};
Person 타입은 User와 Admin의 속성을 모두 가져야 한다.
즉, name, age, role 속성이 필수가 된다.
② 교차 타입을 활용한 객체
type Developer = {
skills: string[];
};
type Manager = {
department: string;
};
type DevManager = Developer & Manager;
const devManager: DevManager = {
skills: ["TypeScript", "React"],
department: "Software Engineering",
};
차 타입을 활용하면, 여러 개의 타입을 결합하여 더욱 정교한 타입을 정의할 수 있다.
타입 가드 (Type Guards)
**타입 가드(Type Guards)**는 런타임에서 특정 타입을 판별하고, 유니온 타입을 안전하게 다룰 수 있도록 도와주는 기능이다.
typeof를 사용한 타입 좁히기 (Type Narrowing)
typeof 연산자를 사용하면 **기본 타입(primitive types)**을 확인할 수 있다.
① typeof를 활용한 조건문
function processValue(value: string | number) {
if (typeof value === "string") {
console.log("문자열 처리:", value.toUpperCase());
} else {
console.log("숫자 연산:", value * 2);
}
}
processValue("hello"); // "문자열 처리: HELLO"
processValue(10); // "숫자 연산: 20"
TypeScript는 typeof 검사 후 자동으로 타입을 좁혀준다.
instanceof를 이용한 객체 타입 확인
instanceof 연산자를 사용하면 클래스 기반 객체의 타입을 확인할 수 있다.
① instanceof를 활용한 타입 체크
class Dog {
bark() {
console.log("멍멍!");
}
}
class Cat {
meow() {
console.log("야옹!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // Dog 타입이므로 안전하게 bark() 호출 가능
} else {
animal.meow(); // Cat 타입이므로 안전하게 meow() 호출 가능
}
}
makeSound(new Dog()); // "멍멍!"
makeSound(new Cat()); // "야옹!"
instanceof를 사용하면 객체의 원래 클래스를 정확하게 판별할 수 있다.
사용자 정의 타입 가드 (is 키워드 사용)
TypeScript에서는 is 키워드를 사용하여 사용자 정의 타입 가드를 만들 수 있다.
이 방법을 사용하면 함수가 특정 타입을 판별하는 역할을 할 수 있다.
① is 키워드를 활용한 타입 가드
type Car = {
brand: string;
drive: () => void;
};
type Bike = {
brand: string;
ride: () => void;
};
// 사용자 정의 타입 가드
function isCar(vehicle: Car | Bike): vehicle is Car {
return (vehicle as Car).drive !== undefined;
}
// 타입 가드를 활용한 함수
function useVehicle(vehicle: Car | Bike) {
if (isCar(vehicle)) {
vehicle.drive(); // Car 타입으로 안전하게 사용 가능
} else {
vehicle.ride(); // Bike 타입으로 안전하게 사용 가능
}
}
const myCar: Car = { brand: "Tesla", drive: () => console.log("Driving...") };
const myBike: Bike = { brand: "Yamaha", ride: () => console.log("Riding...") };
useVehicle(myCar); // "Driving..."
useVehicle(myBike); // "Riding..."
isCar(vehicle)을 호출하면 vehicle이 Car 타입인지 확인한 후, 타입이 자동으로 좁혀진다.
제네릭 (Generics)
**제네릭(Generics)**은 타입을 변수처럼 사용하여 다양한 타입을 유연하게 다룰 수 있도록 하는 기능이다.
기본 사용법 (Generic Function)
제네릭을 사용하면 함수의 매개변수와 반환 타입을 호출 시점에 결정할 수 있다.
제네릭 타입 변수는 <T>처럼 대문자로 표기하는 것이 일반적이다.
① 기본적인 제네릭 함수
function identity<T>(arg: T): T {
return arg;
}
console.log(identity<string>("Hello")); // "Hello"
console.log(identity<number>(42)); // 42
<T>는 매개변수 arg의 타입을 호출할 때 결정하며, 반환 타입도 동일하다.
② 타입 추론 (Type Inference)
TypeScript는 전달된 값에 따라 자동으로 타입을 추론할 수 있다.
console.log(identity("Hello")); // "Hello" (T = string)
console.log(identity(42)); // 42 (T = number)
명시적으로 <string>을 붙이지 않아도 TypeScript가 타입을 추론한다.
제네릭 인터페이스, 클래스, 함수
① 제네릭 인터페이스
제네릭을 인터페이스에서 사용할 수 있다.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "Hello" };
const numberBox: Box<number> = { value: 42 };
console.log(stringBox.value); // "Hello"
console.log(numberBox.value); // 42
Box<T>는 value의 타입을 유연하게 설정할 수 있도록 만든다.
② 제네릭 클래스
클래스에서도 제네릭을 활용할 수 있다.
class Container<T> {
private _value: T;
constructor(value: T) {
this._value = value;
}
getValue(): T {
return this._value;
}
}
const stringContainer = new Container<string>("TypeScript");
const numberContainer = new Container<number>(100);
console.log(stringContainer.getValue()); // "TypeScript"
console.log(numberContainer.getValue()); // 100
제네릭을 활용하면 다양한 타입의 데이터를 저장하는 클래스를 쉽게 만들 수 있다.
③ 제네릭을 활용한 유틸 함수
배열을 받아서 첫 번째 요소를 반환하는 함수.
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
console.log(getFirstElement([1, 2, 3])); // 1
console.log(getFirstElement(["a", "b", "c"])); // "a"
T[] 타입을 받으면, T 타입을 반환하도록 지정한다.
제약 조건 (Constraints, <T extends ...>)
제네릭 타입을 특정 타입으로 제한하고 싶을 때 extends를 사용할 수 있다.
① 객체 속성을 가진 제네릭
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
console.log(getLength("Hello")); // 5 (문자열은 length 속성이 있음)
console.log(getLength([1, 2, 3])); // 3 (배열도 length 속성이 있음)
// console.log(getLength(42)); // ❌ 오류: number는 length 속성이 없음
T extends { length: number }를 사용하여, length 속성이 있는 타입만 허용한다.
② 제네릭 인터페이스에서의 제약 조건
interface Person {
name: string;
}
function greet<T extends Person>(person: T): string {
return `Hello, ${person.name}`;
}
const user = { name: "Alice", age: 25 };
console.log(greet(user)); // "Hello, Alice"
// console.log(greet(42)); // ❌ 오류: number에는 name 속성이 없음
제네릭을 사용하되, Person 타입을 확장한 객체만 허용하도록 제한한다.
타입 유틸리티 (Utility Types)
TypeScript의 **유틸리티 타입(Utility Types)**은 기존 타입을 변형하여 더 유연하고 효율적인 타입을 정의할 수 있도록 도와준다.
이러한 유틸리티 타입을 활용하면 불필요한 코드 작성을 줄이고, 타입 안정성을 유지할 수 있다.
기본 유틸리티 타입
① Partial<T> (모든 속성을 선택적으로 변환)
Partial<T>는 객체 타입의 모든 속성을 선택적(optional)으로 변환한다.
interface User {
name: string;
age: number;
}
const updateUser = (user: Partial<User>) => {
console.log(user);
};
updateUser({ name: "Alice" }); // ✅ age는 생략 가능
updateUser({ age: 25 }); // ✅ name도 생략 가능
User 타입의 속성 name과 age가 선택적(optional)으로 변환된다.
② Required<T> (모든 속성을 필수로 변환)
Required<T>는 객체 타입의 모든 속성을 필수(required)로 변환한다.
interface User {
name?: string;
age?: number;
}
const user: Required<User> = {
name: "Alice",
age: 30,
}; // ✅ name, age가 필수 속성이 됨
기존에 선택적 속성이었던 속성들이 필수 속성으로 변환된다.
③ Readonly<T> (모든 속성을 읽기 전용으로 변환)
Readonly<T>는 객체 타입의 모든 속성을 읽기 전용(readonly)으로 변환한다.
interface User {
name: string;
age: number;
}
const user: Readonly<User> = {
name: "Alice",
age: 30,
};
// user.age = 31; // ❌ 오류: 'age'는 읽기 전용 속성입니다.
객체의 속성을 변경할 수 없도록 보호할 때 유용하다.
④ Record<K, T> (객체 타입을 동적으로 생성)
Record<K, T>는 키(K)와 값(T) 타입을 지정하여 객체를 생성할 수 있다.
type UserRoles = Record<string, string>;
const roles: UserRoles = {
admin: "Alice",
editor: "Bob",
viewer: "Charlie",
};
console.log(roles.admin); // "Alice"
Record<string, string>을 사용하면 **객체의 모든 키는 string, 값도 string**이어야 한다.
타입 변형 유틸리티
⑤ Pick<T, K> (특정 속성만 선택)
Pick<T, K>는 객체 타입에서 특정 속성만 선택하여 새로운 타입을 만든다.
interface User {
name: string;
age: number;
email: string;
}
type UserInfo = Pick<User, "name" | "email">;
const user: UserInfo = {
name: "Alice",
email: "alice@example.com",
};
console.log(user.name); // "Alice"
User 타입에서 name과 email만 선택하여 새로운 타입을 정의했다.
⑥ Omit<T, K> (특정 속성만 제외)
Omit<T, K>는 객체 타입에서 특정 속성을 제외하여 새로운 타입을 만든다.
type UserWithoutEmail = Omit<User, "email">;
const user: UserWithoutEmail = {
name: "Alice",
age: 30,
};
// console.log(user.email); // ❌ 오류: 'email' 속성이 제거됨
email 속성이 제거된 새로운 타입이 생성되었다.
⑦ Exclude<T, U> (유니온 타입에서 특정 타입 제외)
Exclude<T, U>는 유니온 타입에서 특정 타입을 제거한다.
type Status = "success" | "error" | "pending";
type ActiveStatus = Exclude<Status, "pending">;
let status: ActiveStatus = "success"; // ✅
// status = "pending"; // ❌ 오류: 'pending'은 제외됨
"pending"이 제거된 "success" | "error" 타입이 생성된다.
⑧ Extract<T, U> (유니온 타입에서 특정 타입만 선택)
Extract<T, U>는 유니온 타입에서 특정 타입만 남긴다.
type Status = "success" | "error" | "pending";
type PendingStatus = Extract<Status, "pending">;
let status: PendingStatus = "pending"; // ✅
// status = "error"; // ❌ 오류: "pending"만 허용됨
Extract<T, U>는 U에 포함된 타입만 선택하여 새로운 타입을 만든다.
조건부 타입 (Conditional Types)
TypeScript의 **조건부 타입(Conditional Types)**은 타입을 동적으로 결정할 수 있는 강력한 기능이다.
이와 함께 infer 키워드를 활용하면 타입을 추론하는 로직을 만들 수 있다.
기본적인 조건부 타입 (T extends U ? X : Y)
기본 문법:
T extends U ? X : Y
- T가 U의 서브타입이면 X를 반환하고, 그렇지 않으면 Y를 반환한다.
① 조건부 타입 예제
type IsString<T> = T extends string ? "문자열" : "문자열 아님";
type A = IsString<string>; // "문자열"
type B = IsString<number>; // "문자열 아님"
type C = IsString<boolean>; // "문자열 아님"
string 타입이면 "문자열", 아니면 "문자열 아님"을 반환하는 타입을 만들었다.
② 유니온 타입에서 조건부 타입 사용
type Status = "success" | "error" | "pending";
type CheckSuccess<T> = T extends "success" ? true : false;
type A = CheckSuccess<"success">; // true
type B = CheckSuccess<"error">; // false
type C = CheckSuccess<"pending">; // false
"success"이면 true, 그 외에는 false를 반환하는 타입을 만들었다.
③ 제네릭 조건부 타입 예제
type TypeCheck<T> = T extends number ? "숫자" : "다른 타입";
type A = TypeCheck<number>; // "숫자"
type B = TypeCheck<string>; // "다른 타입"
type C = TypeCheck<boolean>; // "다른 타입"
조건부 타입을 활용하여 동적으로 타입을 변환할 수 있다.
infer 키워드 활용 (타입 추론)
infer 키워드는 조건부 타입 내에서 타입을 추론하는 데 사용된다.
이는 특히 함수의 반환 타입이나 제네릭 타입을 추론할 때 유용하다.
① 배열 요소 타입 추출
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<number[]>; // number
type B = ElementType<string[]>; // string
type C = ElementType<boolean[]>; // boolean
type D = ElementType<number>; // number (배열이 아니므로 그대로 반환)
infer U를 사용하여 배열의 요소 타입을 추출할 수 있다.
② 함수의 반환 타입 추출
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function hello(): string {
return "Hello, TypeScript!";
}
type A = ReturnType<typeof hello>; // string
infer R을 사용하여 함수의 반환 타입을 자동으로 추론한다.
③ Promise 내부 값 추출
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<string>; // string (Promise가 아니므로 그대로 반환)
Promise<T>의 내부 값을 자동으로 추출할 수 있다.
keyof 및 매핑된 타입 (Mapped Types)
TypeScript에서는 객체 타입의 키를 동적으로 참조하거나 변환하는 기능이 필요할 때 keyof와 **매핑된 타입(Mapped Types)**을 활용할 수 있다.
이를 통해 유연하면서도 타입 안전성을 유지하는 코드를 작성할 수 있다.
keyof를 사용한 객체 키 타입 추출
① 객체 타입의 키를 가져오기 (keyof)
keyof 연산자를 사용하면 객체 타입에서 키(key)들을 유니온 타입으로 추출할 수 있다.
interface User {
id: number;
name: string;
age: number;
}
type UserKeys = keyof User; // "id" | "name" | "age"
let key: UserKeys;
key = "id"; // ✅ 정상
key = "name"; // ✅ 정상
key = "age"; // ✅ 정상
// key = "email"; // ❌ 오류: 'email'은 User 타입에 없음
keyof User를 사용하면 User 객체의 키들만 할당 가능하다.
② keyof를 활용한 동적 객체 접근
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: "Alice", age: 25 };
console.log(getValue(user, "name")); // "Alice"
console.log(getValue(user, "age")); // 25
// console.log(getValue(user, "email")); // ❌ 오류 발생
K extends keyof T를 사용하여 객체의 키만을 안전하게 접근하도록 제한할 수 있다.
매핑된 타입 (Mapped Types)
매핑된 타입을 사용하면 기존 객체 타입을 변형하여 새로운 타입을 생성할 수 있다.
[P in keyof T] 문법을 사용하여 객체의 각 키를 순회하면서 타입을 변형할 수 있다.
① 모든 속성을 readonly로 변환
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
const readonlyUser: ReadonlyUser = { id: 1, name: "Alice", age: 30 };
// readonlyUser.age = 31; // ❌ 오류: 'age'는 읽기 전용 속성입니다.
모든 속성을 readonly로 변환하는 새로운 타입을 만들었다.
② 모든 속성을 선택적(optional)로 변환
type PartialUser = {
[P in keyof User]?: User[P];
};
const user1: PartialUser = { name: "Alice" }; // ✅ 일부 속성만 포함 가능
객체의 모든 속성을 선택적으로 만들었다.
(이는 Partial<T> 유틸리티 타입과 동일한 역할을 한다.)
③ 모든 속성을 특정 타입으로 변환
type StringifiedUser = {
[P in keyof User]: string;
};
const user2: StringifiedUser = {
id: "1", // ✅ 모든 속성을 string 타입으로 변환
name: "Alice",
age: "30",
};
모든 속성을 string 타입으로 변환하는 타입을 만들었다.
동적 객체 키 타입 처리 (Record<K, T>)
Record<K, T>는 객체의 키(Key)와 값(Value)의 타입을 동적으로 지정할 때 사용한다.
type ScoreBoard = Record<string, number>;
const scores: ScoreBoard = {
Alice: 95,
Bob: 88,
Charlie: 100,
};
console.log(scores.Alice); // 95
모든 키는 string, 값은 number 타입이어야 한다.
Record<K, T>와 keyof를 활용한 객체 타입 변환
type UserRole = "admin" | "editor" | "viewer";
type UserPermissions = Record<UserRole, boolean>;
const permissions: UserPermissions = {
admin: true,
editor: false,
viewer: true,
};
console.log(permissions.admin); // true
객체의 키를 UserRole 유니온 타입으로 제한하여 더 타입 안전한 코드가 되었다.
'JavaScript > TypeScript' 카테고리의 다른 글
모듈과 네임스페이스, 프로젝트 설정 및 최적화 (2) | 2025.03.19 |
---|---|
객체 지향 프로그래밍 (OOP) in TypeScript (0) | 2025.03.19 |
TypeScript의 타입 시스템 (0) | 2025.03.19 |
TypeScript 기본 개념 (0) | 2025.03.19 |
JavaScript & TypeScript 연산자 (0) | 2025.03.05 |