본문 바로가기
JavaScript/TypeScript

함수와 고급 타입 시스템

by curious week 2025. 3. 19.

함수와 고급 타입 시스템 (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 유니온 타입으로 제한하여 더 타입 안전한 코드가 되었다.