TypeScript 타입 시스템의 핵심 구조 – Primitive에서 Discriminated Union까지
Primitive부터 discriminated union까지 어떤 타입을 언제 써야 유지보수가 쉬운지에 대한 판단 기준을 제시
Posted
By okorion
TypeScript 타입 시스템의 핵심 구조 – Primitive에서 Discriminated Union까지
TypeScript 타입 시스템의 본질은 “값이 가질 수 있는 상태를 코드로 고정하는 것”이다
TypeScript의 타입은 문법 장식이 아니라 값의 가능 범위를 제한해 사고 비용을 줄이는 도구다. 이 글은 개별 문법이 아니라, 언제 어떤 타입을 선택해야 유지보수가 쉬워지는지를 기준으로 설명한다.
Primitive 타입은 가장 단순하지만, 가장 많이 오해된다
결론: Primitive 타입은 “값 자체”를 표현하며, 잘못 쓰면 타입 안정성이 가장 먼저 무너진다.
정의
string,number,boolean,null,undefined,symbol,bigint- JS의 원시값을 그대로 반영한 타입
언제 쓰나 (판단 기준)
- 값의 범위가 명확하고, 구조가 필요 없는 경우
- 로컬 계산, 단순 플래그, 옵션 값
흔한 실수
String,Number,Boolean같은 래퍼 타입 사용null | undefined를 고려하지 않은 설계
예제
1
2
3
4
5
let count: number = 0; // ✅
let title: string = "TS"; // ✅
// ❌ 잘못된 사용
let name: String = "kim"; // 객체 래퍼 타입
Object 타입은 “있다/없다”가 아니라 “구조”를 표현해야 한다
결론: object는 거의 쓰지 말고, 항상 구체적인 shape를 정의하라.
정의
- 객체의 프로퍼티 구조를 명시적으로 표현하는 타입
언제 쓰나
- 프로퍼티 이름과 타입이 고정된 데이터 모델
- 함수 인자, 상태, API 응답 모델
흔한 실수
object또는{}로 뭉뚱그려 타입 선언- optional(
?) 남발로 실제 상태가 불분명해짐
예제
1
2
3
4
5
type User = {
id: string;
name: string;
age?: number; // 정말 선택적인 경우만
};
Array 타입은 “동질성”, Tuple은 “위치 기반 의미”를 가진다
결론: 길이와 의미가 고정되면 Array가 아니라 Tuple이다.
정의
T[]/Array<T>: 동일 타입의 나열- Tuple: 고정 길이 + 각 위치의 의미가 다름
언제 쓰나
- Array: 리스트, 컬렉션
- Tuple: 좌표, 페어, API 응답의 위치 기반 값
흔한 실수
- Tuple로 써야 할 걸 Array로 둬서 순서 버그 발생
- Tuple을 push로 확장해 의미 파괴
예제 (Tuple이 필요한 사례)
1
2
3
4
// 위도, 경도는 순서가 의미를 가진다
type LatLng = [number, number];
const location: LatLng = [37.5665, 126.9780];
Enum은 “편리하지만 비용이 있다”, Union Literal이 기본값이다
결론: Enum은 런타임 코드가 필요할 때만 쓰고, 대부분은 union literal이 낫다.
정의
enum: TS 컴파일 결과에 실제 JS 객체가 생성됨- union literal: 컴파일 후 사라지는 타입 제약
언제 쓰나
- Enum: 런타임에서도 값 집합이 필요할 때
- Union literal: 상태/옵션 제한용 (대부분의 경우)
흔한 실수
- 단순 상태 표현에 enum 남용 → 번들 증가, 디버깅 복잡
예제 (Enum vs Union Literal)
1
2
3
4
5
6
7
8
// Enum
enum RoleEnum {
Admin,
User,
}
// Union Literal (권장)
type Role = "admin" | "user";
Union과 Literal은 “상태를 타입으로 표현”하는 핵심 도구다
결론: if/flag로 처리하던 분기를 타입으로 끌어올려라.
정의
- Union: 여러 타입 중 하나
- Literal: 값 자체를 타입으로 고정
언제 쓰나
- 상태, 모드, 단계(step), 권한, UI 분기
흔한 실수
- boolean으로 상태를 표현해 확장 불가능해짐
- string을 풀어놓고 비교만으로 제어
예제
1
2
3
4
5
6
7
type Status = "idle" | "loading" | "success" | "error";
function render(status: Status) {
if (status === "loading") {
// ...
}
}
Type Alias vs Interface는 “확장 방식과 목적”이 다르다
결론: 데이터 모델은 type, 계약(구현 강제)은 interface가 기본 전략이다.
정의
type: 모든 타입 표현 가능 (union, primitive 포함)interface: 객체 구조 + 선언 병합 가능
언제 쓰나
- type: API 모델, 상태, 유니언
- interface: 클래스 구현 계약, 라이브러리 공개 타입
흔한 실수
- 둘을 스타일 차이로만 인식
- union을 interface로 표현하려다 막힘
예제
1
2
3
4
5
type ApiResponse = { data: string } | { error: string };
interface Repository {
save(data: string): void;
}
Discriminated Union은 “상태 기반 로직”의 최종 형태다
결론: 상태가 3개 이상이면 Discriminated Union이 가장 안전하다.
정의
- 공통 식별자(
kind,type)를 가진 union 타입
언제 쓰나
- UI 상태, 비즈니스 상태 머신, API 응답 분기
흔한 실수
- 식별자 없이 union만 사용 → 타입 좁히기 불가
- switch에서 default 처리 누락
예제 (exhaustive check 포함)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type UIState =
| { type: "idle" }
| { type: "loading" }
| { type: "error"; message: string };
function assertNever(x: never): never {
throw new Error("Unhandled state");
}
function render(state: UIState) {
switch (state.type) {
case "idle":
return "대기";
case "loading":
return "로딩";
case "error":
return state.message;
default:
return assertNever(state);
}
}
Optional Chaining과 Nullish Coalescing은 “타입 경계에서만” 써야 한다
결론: 편의 문법이지, 설계 대체물이 아니다.
예제
1
2
3
4
5
6
7
type User = {
profile?: {
nickname?: string;
};
};
const name = user.profile?.nickname ?? "anonymous";
주의: 내부 로직에서 남발하면 “왜 없는지”를 숨긴다.
“이걸 쓰면 좋은데, 이 경우엔 쓰지 마라”
- Tuple ✔ 좌표/고정 포맷 / ❌ 가변 리스트
- Enum ✔ 런타임 필요 / ❌ 단순 상태
- Union ✔ 상태 분기 / ❌ 복잡한 객체 상속
- optional ✔ 진짜 선택 / ❌ 귀찮아서
체크리스트
- boolean 대신 union으로 상태를 표현했는가
- 고정 길이/의미 데이터에 tuple을 썼는가
- enum이 정말 런타임에 필요한가
- 외부/상태 분기에 discriminated union을 썼는가
- switch에 exhaustive check가 있는가
요약 5줄
- 타입은 값의 “가능한 상태”를 제한하는 도구다.
- Primitive/Object/Array/Tuple은 의미 단위로 선택해야 한다.
- Enum은 비용이 있고, union literal이 기본값이다.
- Union과 Literal은 상태를 타입으로 끌어올린다.
- Discriminated Union은 확장 가능한 상태 모델의 정답이다.
자기점검 질문 5개
- 현재 프로젝트에서 boolean으로 표현된 상태는 무엇인가?
- enum을 쓰는 이유가 “편해서”인지 “런타임 필요”인지 구분되는가?
- Array로 표현된 데이터 중 Tuple이 더 적절한 것은 없는가?
- 상태 분기 switch에 exhaustive check가 있는가?
- optional이 진짜 선택적인 의미를 가지는가?
실전 미션 3개
- boolean 상태 하나를 union + discriminated union으로 리팩터링하라.
- enum 하나를 union literal로 교체하고 번들 차이를 확인하라.
- API 응답 모델을 discriminated union으로 다시 정의해보라.
This post is licensed under CC BY 4.0 by the author.
