문법이 아니라 실패 모드에서 패턴을 고른다

타입 복잡도는 UI에서 실제로 막아야 할 잘못된 상태만큼만 쓴다.

01

불가능한 UI 조합

문제 loading인데 data와 error가 함께 존재한다.

type Load<T> =
  | { state: "loading" }
  | { state: "error"; error: Error }
  | { state: "success"; data: T };

switch (load.state) { /* narrow */ }

결과 판별자 하나로 필드가 좁혀지고 never로 누락 분기를 확인한다.

02

컴포넌트 계약 단절

문제 items와 render·select의 항목 타입이 따로 논다.

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => ReactNode;
  onSelect: (item: T) => void;
};

function List<T>(p: ListProps<T>) {}

결과 한 번 추론된 T가 입력·렌더·이벤트 끝까지 같은 관계를 유지한다.

03

설정 검증과 리터럴 손실

문제 키 오타는 잡되 값이 string으로 넓어지면 안 된다.

const theme = {
  primary: "#3178c6",
  danger: "#dc2626",
} as const satisfies
  Record<"primary" | "danger", string>;

결과 as const는 리터럴을 보존하고 satisfies는 호환성만 검사한다.

컴파일 경계 타입은 앱 내부의 잘못된 조합을 막는다. API·스토리지처럼 외부에서 들어오는 값은 런타임 검증을 통과시킨 뒤 타입 세계로 넣는다.