TypeScript 독학 가이드 7 - 타입 별칭과 유니온 타입
“이 함수는 문자열이나 숫자를 받아요” 이런 경우 어떻게 타입을 지정할까요? 유니온 타입이 답이에요. 그리고 긴 타입을 매번 쓰기 귀찮을 때는? 타입 별칭을 쓰면 됩니다. 오늘은 TypeScript의 고급 타입 기능들을 알아볼게요.
타입 별칭 (Type Alias)
긴 타입을 짧은 이름으로 만드는 거예요. type 키워드를 사용합니다.
type UserID = number;
type UserName = string;
let id: UserID = 123;
let name: UserName = "김철수";
console.log(id);
console.log(name);
실행 결과:
타입 별칭으로 의미를 명확하게 표현할 수 있습니다
객체 타입 별칭
객체 타입도 별칭으로 만들 수 있어요:
type User = {
id: number;
name: string;
email: string;
};
const user: User = {
id: 1,
name: "김철수",
email: "kim@example.com"
};
function printUser(user: User) {
console.log(`ID: ${user.id}, 이름: ${user.name}`);
}
printUser(user);
실행 결과:
복잡한 객체 타입을 재사용할 수 있습니다
유니온 타입 (Union Type)
“A 또는 B” 이런 식으로 여러 타입 중 하나를 받을 수 있어요. | 기호를 사용합니다.
type ID = number | string;
function printID(id: ID) {
console.log(`ID: ${id}`);
}
printID(123);
printID("ABC123");
printID(true); // 에러! boolean은 안 됨
실행 결과:
number 또는 string만 허용됩니다
타입 가드
유니온 타입 사용할 때 타입을 구분해야 할 때가 있어요:
function processValue(value: number | string) {
if (typeof value === "string") {
console.log(`문자열 길이: ${value.length}`);
} else {
console.log(`숫자 2배: ${value * 2}`);
}
}
processValue("TypeScript");
processValue(10);
실행 결과:
typeof로 타입을 체크하고 각각 다르게 처리합니다
typeof로 타입을 확인하면 TypeScript가 그 블록 안에서는 타입을 좁혀줘요. 이걸 “타입 가드”라고 합니다.
배열 유니온
type StringOrNumber = string | number;
type ArrayOfStringOrNumber = StringOrNumber[];
const values: ArrayOfStringOrNumber = [1, "hello", 2, "world"];
values.forEach(value => {
if (typeof value === "string") {
console.log(`문자열: ${value}`);
} else {
console.log(`숫자: ${value}`);
}
});
실행 결과:
문자열과 숫자가 섞인 배열을 처리할 수 있습니다
인터섹션 타입 (Intersection Type)
“A 그리고 B” 두 타입을 합치는 거예요. & 기호를 사용합니다.
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: string;
department: string;
};
type WorkingPerson = Person & Employee;
const worker: WorkingPerson = {
name: "김철수",
age: 25,
employeeId: "E001",
department: "개발팀"
};
console.log(worker);
실행 결과:
두 타입의 모든 속성을 가져야 합니다
WorkingPerson은 Person의 속성 + Employee의 속성을 전부 가져야 해요.
리터럴 타입 (Literal Type)
정확한 값 자체를 타입으로 쓸 수 있어요.
문자열 리터럴
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction) {
console.log(`${direction} 방향으로 이동`);
}
move("up");
move("down");
move("diagonal"); // 에러!
실행 결과:
정확히 지정된 값만 사용할 수 있습니다
정확히 “up”, “down”, “left”, “right” 중 하나만 받아요. 다른 문자열은 에러!
숫자 리터럴
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): DiceRoll {
return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
}
const result = rollDice();
console.log(`주사위 결과: ${result}`);
실행 결과:
1부터 6까지의 숫자만 반환합니다
불린 리터럴
type Success = true;
type Failure = false;
function isSuccess(result: Success | Failure): string {
return result ? "성공!" : "실패...";
}
console.log(isSuccess(true));
console.log(isSuccess(false));
실행 결과:
true 또는 false 리터럴 타입을 사용할 수 있습니다
객체 리터럴 타입
객체의 정확한 구조를 타입으로 지정할 수 있어요:
type Config = {
readonly apiUrl: string;
timeout: number;
retryCount?: number;
};
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000
};
console.log(config);
// config.apiUrl = "다른 URL"; // 에러! readonly
실행 결과:
readonly 속성은 수정할 수 없습니다
타입 별칭 vs 인터페이스
“언제 type 쓰고 언제 interface 써요?” 자주 받는 질문이에요.
type으로만 가능한 것
// 유니온 타입
type ID = number | string;
// 튜플
type Point = [number, number];
// 프리미티브 타입 별칭
type Name = string;
interface로만 가능한 것
// 선언 병합
interface User {
name: string;
}
interface User {
age: number;
}
// 자동으로 합쳐짐
공통점
객체 타입은 둘 다 가능:
type UserType = {
name: string;
age: number;
};
interface UserInterface {
name: string;
age: number;
}
추천: 객체는 interface, 유니온/튜플은 type!
실전 예제: HTTP 응답 타입
API 작업할 때 유용한 패턴이에요:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type SuccessResponse<T> = {
success: true;
data: T;
};
type ErrorResponse = {
success: false;
error: {
code: string;
message: string;
};
};
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
type User = {
id: number;
name: string;
email: string;
};
function fetchUser(id: number): ApiResponse<User> {
// 성공 케이스
return {
success: true,
data: {
id: 1,
name: "김철수",
email: "kim@example.com"
}
};
}
const response = fetchUser(1);
if (response.success) {
console.log(`사용자: ${response.data.name}`);
} else {
console.log(`에러: ${response.error.message}`);
}
실행 결과:
유니온 타입으로 성공/실패 케이스를 명확히 구분했습니다
실전 예제: 상태 관리
React나 Vue 같은 프레임워크에서 자주 쓰는 패턴:
type LoadingState = {
status: "loading";
};
type SuccessState<T> = {
status: "success";
data: T;
};
type ErrorState = {
status: "error";
error: string;
};
type State<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState(state: State<string>) {
switch (state.status) {
case "loading":
console.log("로딩 중...");
break;
case "success":
console.log(`데이터: ${state.data}`);
break;
case "error":
console.log(`에러: ${state.error}`);
break;
}
}
handleState({ status: "loading" });
handleState({ status: "success", data: "완료!" });
handleState({ status: "error", error: "네트워크 오류" });
실행 결과:
상태에 따라 타입이 자동으로 좁혀집니다
status로 switch하면 TypeScript가 각 case에서 타입을 자동으로 좁혀줘요. 엄청 편합니다!
정리하면
- 타입 별칭:
type키워드로 타입에 이름 붙이기 - 유니온 타입:
|로 “또는” 표현 - 인터섹션 타입:
&로 “그리고” 표현 - 리터럴 타입: 정확한 값을 타입으로 사용
- 타입 가드: typeof, switch 등으로 타입 좁히기
다음 글에서는 제네릭에 대해 배워볼게요. 함수나 클래스를 만들 때 타입을 유연하게 받는 방법입니다!