본문으로 건너뛰기
Advertisement

3.2 타입 별칭

타입 별칭(type alias)은 type 키워드로 기존 타입에 새로운 이름을 붙이거나, 여러 타입을 조합해 복잡한 새 타입을 만드는 도구다. 단순히 이름을 바꾸는 것을 넘어, 유니온(|)·인터섹션(&)·리터럴 타입 등 TypeScript 타입 시스템의 표현력을 최대한 활용할 수 있게 해준다.

인터페이스가 "이 객체는 이런 구조다"라는 계약이라면, 타입 별칭은 "이 타입은 저런 여러 타입들의 조합이다"라는 수식(formula)에 가깝다.


type 키워드 기본 사용

원시 타입에 별칭 붙이기

type UserId = string;
type Age = number;
type IsActive = boolean;

// 원시 타입 별칭: 코드 의도를 명확히 전달
const userId: UserId = "user-abc-123";
const age: Age = 28;

// 함수 매개변수에서 의미 전달
function getUserById(id: UserId): Promise<User> {
// UserId임을 명시해 실수로 다른 string을 전달하는 것을 방지
return fetch(`/api/users/${id}`).then((r) => r.json());
}

객체 타입 별칭

type Coordinate = {
x: number;
y: number;
z?: number;
};

type Rectangle = {
topLeft: Coordinate;
bottomRight: Coordinate;
};

const rect: Rectangle = {
topLeft: { x: 0, y: 0 },
bottomRight: { x: 100, y: 50 },
};

함수 타입 별칭

// 화살표 함수 스타일로 함수 타입 정의
type Predicate<T> = (value: T) => boolean;
type Transformer<T, U> = (input: T) => U;
type EventHandler<T> = (event: T) => void;
type AsyncOperation<T, U> = (input: T) => Promise<U>;

// 사용 예
const isPositive: Predicate<number> = (n) => n > 0;
const toString: Transformer<number, string> = (n) => String(n);

function filter<T>(arr: T[], predicate: Predicate<T>): T[] {
return arr.filter(predicate);
}

const positives = filter([1, -2, 3, -4, 5], isPositive); // [1, 3, 5]

유니온 타입 (|)

유니온 타입은 "A이거나 B이거나 C일 수 있다"는 OR 관계를 표현한다.

기본 유니온

type StringOrNumber = string | number;
type NullableString = string | null;
type MaybeUser = User | null | undefined;

function formatId(id: StringOrNumber): string {
return typeof id === "number" ? id.toString() : id;
}

타입 가드와의 결합

유니온 타입을 사용할 때는 어떤 타입인지 좁혀주는 타입 가드가 필수다.

type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };

function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}

// exhaustiveness check: 모든 케이스 처리 강제
function assertNever(value: never): never {
throw new Error(`처리되지 않은 케이스: ${JSON.stringify(value)}`);
}

function describeShape(shape: Shape): string {
switch (shape.kind) {
case "circle":
return `반지름 ${shape.radius}인 원`;
case "rectangle":
return `${shape.width}×${shape.height} 직사각형`;
case "triangle":
return `밑변 ${shape.base}, 높이 ${shape.height}인 삼각형`;
default:
return assertNever(shape); // 새 Shape 추가 시 컴파일 오류로 알림
}
}

typeof 타입 가드

type InputValue = string | number | boolean | null;

function processInput(value: InputValue): string {
if (value === null) return "null 값";
if (typeof value === "boolean") return value ? "참" : "거짓";
if (typeof value === "number") return `숫자: ${value.toFixed(2)}`;
return `문자열: "${value}"`;
}

인터섹션 타입 (&)

인터섹션 타입은 "A이면서 동시에 B이기도 하다"는 AND 관계를 표현한다. 여러 타입의 프로퍼티를 모두 합산한다.

기본 인터섹션

type Timestamped = {
createdAt: Date;
updatedAt: Date;
};

type SoftDeletable = {
deletedAt: Date | null;
isDeleted: boolean;
};

type BaseModel = Timestamped & SoftDeletable;

// BaseModel은 createdAt, updatedAt, deletedAt, isDeleted를 모두 가짐
const model: BaseModel = {
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
isDeleted: false,
};

믹스인(Mixin) 패턴

인터섹션 타입을 이용해 여러 기능을 조합하는 믹스인 패턴을 구현할 수 있다.

type Identifiable = { id: string };
type Named = { name: string };
type Emailable = { email: string };
type Roleable = { role: string };

type BasicUser = Identifiable & Named & Emailable;
type AuthUser = BasicUser & Roleable & { lastLoginAt: Date };

// 제네릭과 결합한 믹스인
type WithPagination<T> = T & {
page: number;
pageSize: number;
total: number;
};

type UserListResponse = WithPagination<{ users: BasicUser[] }>;

const response: UserListResponse = {
users: [],
page: 1,
pageSize: 20,
total: 0,
};

충돌하는 인터섹션

같은 프로퍼티 이름에 다른 타입이 교차하면 never가 된다.

type A = { value: string };
type B = { value: number };
type AB = A & B;

// AB.value는 string & number = never
// 실질적으로 AB 타입의 객체를 만들 수 없음
const x: AB = { value: "hi" as never }; // 강제 캐스팅 필요

리터럴 타입

리터럴 타입은 특정 값 자체를 타입으로 사용한다. 정확한 값만 허용할 때 유용하다.

문자열 리터럴

type Direction = "north" | "south" | "east" | "west";
type Alignment = "left" | "center" | "right";
type Status = "pending" | "active" | "suspended" | "deleted";

function move(direction: Direction, steps: number): void {
console.log(`${direction} 방향으로 ${steps}걸음 이동`);
}

move("north", 5); // 정상
// move("up", 3); // 오류: "up"은 Direction이 아님

숫자 리터럴

type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
type HttpSuccessCode = 200 | 201 | 204;
type HttpErrorCode = 400 | 401 | 403 | 404 | 422 | 500;
type HttpStatusCode = HttpSuccessCode | HttpErrorCode;

function rollDice(): DiceValue {
return (Math.floor(Math.random() * 6) + 1) as DiceValue;
}

불리언 리터럴

// 불리언 리터럴은 조건부 타입과 함께 주로 사용
type IsAdmin<T> = T extends { role: "admin" } ? true : false;

const assertion으로 리터럴 타입 고정

// 일반 선언: 타입이 widened됨
const direction1 = "north"; // 타입: string (아님: "north")
let direction2 = "north"; // 타입: string

// const assertion: 리터럴 타입 유지
const direction3 = "north" as const; // 타입: "north"

const config = {
method: "POST",
path: "/api/users",
} as const;
// config.method 타입: "POST" (string 아님)
// config.path 타입: "/api/users" (string 아님)

유틸리티 타입과 type alias 결합

TypeScript 내장 유틸리티 타입을 type 별칭과 결합하면 반복적인 타입 코드를 줄일 수 있다.

type User = {
id: string;
username: string;
email: string;
password: string;
role: "user" | "admin";
isActive: boolean;
createdAt: Date;
};

// Partial: 모든 프로퍼티를 optional로
type UserUpdatePayload = Partial<Omit<User, "id" | "createdAt">>;

// Pick: 특정 프로퍼티만 선택
type UserPublicInfo = Pick<User, "id" | "username" | "role">;

// Required: 모든 optional을 필수로
type StrictConfig = Required<{
apiUrl?: string;
timeout?: number;
retries?: number;
}>;
// { apiUrl: string; timeout: number; retries: number; }

// Readonly: 모든 프로퍼티를 readonly로
type FrozenUser = Readonly<User>;

// Exclude: 유니온에서 특정 타입 제거
type NonAdminRole = Exclude<User["role"], "admin">; // "user"

// Extract: 유니온에서 특정 타입만 추출
type AdminRole = Extract<User["role"], "admin">; // "admin"

실전 예제: API 응답 타입, HTTP 메서드 리터럴, 상태 머신

HTTP 메서드와 API 응답 타입

// HTTP 메서드 리터럴 타입
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";

// 성공/실패 응답 유니온
type ApiSuccess<T> = {
ok: true;
data: T;
message?: string;
};

type ApiError = {
ok: false;
error: string;
code: HttpErrorCode;
details?: Record<string, string[]>;
};

type ApiResponse<T> = ApiSuccess<T> | ApiError;

// 사용 예
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { ok: false, error: "사용자를 찾을 수 없음", code: 404 };
}
const data = await res.json();
return { ok: true, data };
} catch {
return { ok: false, error: "네트워크 오류", code: 500 };
}
}

// 타입 가드로 응답 처리
async function displayUser(id: string): Promise<void> {
const response = await fetchUser(id);
if (response.ok) {
console.log(`사용자: ${response.data.username}`);
} else {
console.error(`오류 [${response.code}]: ${response.error}`);
}
}

상태 머신(State Machine)

// 주문 상태 머신
type OrderStatus =
| "cart"
| "pending_payment"
| "payment_confirmed"
| "preparing"
| "shipped"
| "delivered"
| "cancelled"
| "refunded";

type OrderTransition = {
from: OrderStatus;
to: OrderStatus;
action: string;
};

// 허용된 상태 전이 정의
const VALID_TRANSITIONS: OrderTransition[] = [
{ from: "cart", to: "pending_payment", action: "checkout" },
{ from: "pending_payment", to: "payment_confirmed", action: "pay" },
{ from: "pending_payment", to: "cancelled", action: "cancel" },
{ from: "payment_confirmed", to: "preparing", action: "confirm" },
{ from: "preparing", to: "shipped", action: "ship" },
{ from: "shipped", to: "delivered", action: "deliver" },
{ from: "delivered", to: "refunded", action: "refund" },
{ from: "preparing", to: "cancelled", action: "cancel" },
];

type Order = {
id: string;
status: OrderStatus;
items: { productId: string; quantity: number; price: number }[];
total: number;
};

function canTransition(
current: OrderStatus,
target: OrderStatus
): boolean {
return VALID_TRANSITIONS.some(
(t) => t.from === current && t.to === target
);
}

function transition(order: Order, target: OrderStatus): Order {
if (!canTransition(order.status, target)) {
throw new Error(
`'${order.status}'에서 '${target}'으로 전이 불가`
);
}
return { ...order, status: target };
}

고수 팁: 복잡한 타입 분해와 가독성 있는 타입 설계

복잡한 타입을 의미 있는 단위로 분해하기

// 나쁜 예: 모든 것이 인라인으로 뭉쳐있음
type BadApiConfig = {
endpoints: Record<
string,
{
method: "GET" | "POST" | "PUT" | "DELETE";
headers: Record<string, string>;
auth: { type: "bearer" | "basic"; token: string } | null;
retry: { maxAttempts: number; backoff: "linear" | "exponential" };
}
>;
};

// 좋은 예: 의미 있는 단위로 분해
type AuthConfig = { type: "bearer" | "basic"; token: string } | null;
type RetryConfig = { maxAttempts: number; backoff: "linear" | "exponential" };
type EndpointConfig = {
method: HttpMethod;
headers: Record<string, string>;
auth: AuthConfig;
retry: RetryConfig;
};
type GoodApiConfig = {
endpoints: Record<string, EndpointConfig>;
};

브랜드 타입(Branded Type)으로 원시 타입 구분

// 같은 string이지만 혼동하면 안 되는 경우
type UserId = string & { readonly __brand: "UserId" };
type ProductId = string & { readonly __brand: "ProductId" };
type OrderId = string & { readonly __brand: "OrderId" };

function createUserId(raw: string): UserId {
// 유효성 검사 가능
if (!raw.startsWith("user-")) throw new Error("잘못된 UserId 형식");
return raw as UserId;
}

// 타입 레벨에서 혼동 방지
function getUser(id: UserId): Promise<User> {
return fetch(`/api/users/${id}`).then((r) => r.json());
}

const uid = createUserId("user-123");
const pid = "product-456" as ProductId;

getUser(uid); // 정상
// getUser(pid); // 오류: ProductId는 UserId가 아님

조건부 타입으로 동적 타입 계산

// 배열이면 요소 타입을 추출, 아니면 그대로
type Unwrap<T> = T extends Array<infer U> ? U : T;

type A = Unwrap<string[]>; // string
type B = Unwrap<number>; // number
type C = Unwrap<User[]>; // User

// Promise를 벗겨내는 타입
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;

type D = Awaited2<Promise<string>>; // string
type E = Awaited2<Promise<Promise<number>>>; // number

정리 표

구문설명예시
type A = string원시 타입 별칭type Name = string
type A = { ... }객체 타입 별칭type Point = { x: number; y: number }
type A = B | C유니온 타입type ID = string | number
type A = B & C인터섹션 타입type Entity = Base & Timestamped
type A = "a" | "b"문자열 리터럴 유니온type Dir = "left" | "right"
type A = 1 | 2 | 3숫자 리터럴 유니온type Dice = 1 | 2 | ... | 6
as const리터럴 타입 고정const x = "hello" as const
Partial<T>모든 프로퍼티 optionaltype Update = Partial<User>
Pick<T, K>프로퍼티 선택type Info = Pick<User, "id" | "name">
Exclude<T, U>유니온에서 제거type NonAdmin = Exclude<Role, "admin">

다음 장에서는...

3.3 interface vs type에서는 두 도구를 직접 비교한다. 확장 방식, 선언 병합 가능 여부, 재귀 타입, 실무 가이드라인까지 "어떤 상황에 무엇을 써야 하는가"를 명확하게 정리한다.

Advertisement