콘텐츠로 이동

실전 타입 트릭 모음

TypeScript를 실무에서 쓰다 보면 기본 타입 시스템만으로는 해결이 어려운 상황을 자주 만납니다. 여기서는 바로 적용 가능한 실전 타입 트릭 네 가지를 소개합니다.

유니온 타입에 string을 추가하면 자동완성이 사라지는 문제가 있습니다.

type IconSize = "sm" | "xs";
interface IconProps {
size: IconSize;
}
export const Icon = (props: IconProps) => {
return <></>;
};
// 자동완성 O: "sm", "xs"만 허용
<Icon size="sm" />

다른 문자열 값도 허용하고 싶어 string을 추가하면:

type IconSize = "sm" | "xs" | string;
// 자동완성 비활성화! string이 유니온을 흡수함

해결법: Omit을 활용해 이미 정의된 리터럴을 string에서 제외합니다.

type IconSize = "sm" | "xs" | Omit<string, "sm" | "xs">;
// 자동완성 유지 + 다른 문자열도 허용

이를 제네릭으로 만들면 재사용 가능합니다.

type LooseAutoComplete<T extends string> = T | Omit<string, T>;

Object.keys()의 반환 타입은 기본적으로 string[]입니다. 이 때문에 키로 객체에 접근하면 any가 됩니다.

export const myObj = {
a: 1,
b: 2,
c: 3,
};
const keys = Object.keys(myObj).map((key) => {
return myObj[key]; // any[]로 추론됨
});

타입 단언을 포함한 래퍼 함수를 만들어 해결할 수 있습니다.

const objectKeys = <Obj extends object>(obj: Obj): (keyof Obj)[] => {
return Object.keys(obj) as (keyof Obj)[];
};
const keys = objectKeys(myObj).map((key) => {
return myObj[key]; // number로 정확히 추론
});

TypeScript는 기본적으로 더 일반적인 타입으로 추론합니다. 배열의 경우 string[]처럼 넓은 타입으로 추론하는데, as const를 사용하면 리터럴 타입으로 좁힐 수 있습니다.

const arr = ["a", "b", "c", "d"]; // string[]
arr[2] = "3"; // 가능
const arr2 = ["a", "b", "c", "d"] as const;
// readonly ["a", "b", "c", "d"]
arr2[2] = "3"; // 에러! readonly

내부 요소를 엄격하게 관리하고 싶을 때, 특히 유니온 타입이나 튜플의 원본으로 사용할 때 유용합니다.

Record나 인덱스 시그니처를 사용할 때, 존재하지 않는 키에 접근해도 타입 에러가 발생하지 않는 문제가 있습니다.

export const myObj: Record<string, string[]> = {};
myObj.foo.push("bar"); // 런타임 에러, 하지만 타입 에러 없음

tsconfig.json에서 이 옵션을 활성화하면:

{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}

인덱스 접근 시 undefined가 포함된 타입으로 추론되어 안전한 접근을 강제합니다.

export const myObj: Record<string, string[]> = {};
// myObj.foo가 undefined일 수 있으므로 optional chaining 필요
myObj.foo?.push("bar");

이 옵션은 strict 모드에 포함되지 않으므로 별도로 활성화해야 합니다. 런타임 에러를 타입 레벨에서 미리 잡을 수 있어 적극 권장합니다.