never 타입은 사용해 본 적이 never예요
TypeScript의 never 타입에 대해 처음 알아보자
2024-10-26
never 타입의 개념
타입은 가능한 값의 집합입니다. 그리고 never
타입은 값의 공집합입니다. 공집합에는 어떤 값도 들어갈 수 없기 때문에 never
타입은 any
타입의 값을 포함해 그 어떤 값도 가질 수 없습니다.
타입스크립트 핸드북에서는 never
타입을 bottom type이라고 정의합니다. 타입 계층 트리로 이해하면 bottom type이 무엇인지 잘 와닿습니다.
never 타입을 사용하는 경우
에러를 던지는 경우
function generateError(res: Response): never {
throw new Error(res.getMessage());
}
특정 함수가 실행 중 마지막에 에러를 던지는 작업을 수행한다면 해당 함수의 반환 타입은 never
입니다.
무한히 함수가 실행되는 경우
function checkStatus(): never {
while (true) {
// ...
}
}
무한 루프는 결국 함수가 종료되지 않음을 의미하기 때문에 값을 반환하지 못합니다. 따라서 해당 함수의 반환 타입은 never
입니다.
Exhaustiveness Checking
// 예제 코드 출처: 우아한 타입스크립트 with 리액트
type ProductPrice = "10000" | "20000" | "5000";
const getProductName = (productPrice: ProductPrice): string => {
if (productPrice === "10000") return "배민상품권 1만 원";
if (productPrice === "20000") return "배민상품권 2만 원";
else {
exhaustiveCheck(productPrice); // Error: Argument of type 'string' is not assignable to parameter of type 'never'
return "배민상품권";
}
};
const exhaustiveCheck = (param: never) => {
throw new Error("Type error!");
};
Exhaustiveness는 사전적으로 철저함, 완전함을 의미합니다. 따라서 Exhaustiveness Checking은 모든 케이스에 대해 철저하게 타입을 검사하는 것을 말하며 타입 좁히기에 사용되는 패러다임 중 하나입니다.
Exhaustiveness Checking을 사용하면 다음과 같은 장점이 있습니다.
- 컴파일 타임에서의 완전성 검사
- exhaustiveCheck 함수를 사용하면 TypeScript 컴파일러가 모든 가능한 값이 처리되었는지 확인합니다. 만약 어떤 경우를 누락했다면 컴파일러는 에러를 발생시켜 개발 단계에서 이를 잡아낼 수 있습니다.
- 유지 보수성 향상
- 새로운 값이 타입에 추가되면, 컴파일 타임 에러를 통해 누락된 처리를 즉시 인지할 수 있어 코드의 유지 보수가 용이합니다.
- 위 예제 코드를 보면 운영 정책 변경으로 인해 만약 1만원권이나 2만원권 이외의 새로운 금액의 상품권이 생겼을 때, 이에 대응하지 않으면 컴파일 타임에 에러를 발생시켜 런타임에서 의도치 않은 동작이 발생하는 것을 방지할 수 있습니다.
- 가독성 및 명시성 증가
- 코드를 읽는 사람에게 모든 가능한 경우를 처리하려는 의도를 명확하게 전달합니다.
- 우아한 타입스크립트 with 리액트에서는 “우형 이야기”라는 섹션이 있는데요, 여기서 exhaustiveCheck가 런타임 코드에 포함되어 버려서 뭔가 프로덕션 코드와 테스트 코드가 같이 섞여 있는 듯한 느낌이 든다는 지적이 있었습니다.
- 하지만 코드상의 어서션은 코드 중간중간에 무조건 특정 값을 가지는 상황을 확인하기 위한 디버깅 또는 주석 느낌으로 사용된다고 보면 다른 개발자들이 그 코드를 볼 때 가독성에 도움을 줄 것은 확실해 보입니다.
부분적으로 구조적 타이핑을 허용하지 않는 방법
type VariantA = {
a: string;
}
type VariantB = {
b: string;
}
declare function fn(arg: VariantA | VariantB): void
const input = { a: 'foo', b: 123 }
fn(input);
타입스크립트는 구조적 타이핑을 기반으로 하고 있기 때문에 위와 같인 원래 타입보다 더 많은 속성을 가진 객체 타입을 함수에 전달하는 것이 허용됩니다. 이는 개발자가 의도하지 않는 방향일 확률이 크죠. 이를 never
타입을 활용해서 방지할 수 있습니다.
type VariantA = {
a: string;
b?: never;
}
type VariantB = {
a?: never;
b: string;
}
declare function fn(arg: VariantA | VariantB): void
const input = { a: 'foo', b: 123 }
fn(input); // 여기서 에러가 발생합니다.
as never 단언으로 해결할 수 있는 문제
💡 글또의 이주함, 이승환, 이정훈, 김성현a님이 도움을 주셨습니다!
다음은 TotalTypeScript라는 사이트에 등장하는 예제입니다.
const formatters = {
string: (input: string) => input.toUpperCase(),
number: (input: number) => input.toFixed(2),
boolean: (input: boolean) => (input ? "true" : "false"),
};
const format = (input: string | number | boolean) => {
const inputType = typeof input as
| "string"
| "number"
| "boolean";
const formatter = formatters[inputType];
return formatter(input);
}
formatters
객체는 string, number, boolean
타입의 키 값이 들어오면 그에 따라 포맷팅 하는 함수를 리턴해줍니다. 또한 format
함수에서 inputType
변수는 input
을 string | number | boolean
타입으로 단언한 그 타입을 값으로 가집니다. 따라서 일반적으로 생각하면 formatter
는 분명히 string, number, boolean
타입 중 하나의 타입을 인자로 받을 것이라고 예상할 수 있습니다. 하지만 formatter(input)
부분에서 Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.
오류가 발생합니다. 그 이유는 무엇일까요?
유니언 타입에서 함수의 인자들은 각 함수 타입의 교차 집합으로 추론됩니다. 즉, 유니언 타입으로 정의된 함수는 공통적으로 적용할 수 있는 타입만 허용합니다.
inputType
은 string | number | boolean
이므로 formatter
는 formatters
에 해당 유니언 타입의 원소들을 넣었을 때 나올 수 있는 모든 타입의 유니언으로 추론됩니다. 따라서 formatter
의 타입은 다음과 같습니다.
((input: string) => string) | ((input: number) => string) | ((input: boolean) => "true" | "false")
따라서 input
은 string & number & boolean
이어야 하는데 이런 값은 없기 때문에 input
의 타입이 nerver
로 추론되고, string | number | boolean
타입의 input
이 할당될 수 없게 됩니다. 그래서 다음과 같이 코드를 작성하면 해결할 수 있습니다.
const format = (input: string | number | boolean) => {
const inputType = typeof input as
| "string"
| "number"
| "boolean";
const formatter = formatters[inputType];
return formatter(input as never);
};
never 타입을 확인하는 방법
type IsNever<T> = T extends never ? true : false
type Res = IsNever<never> // never 🧐
never
타입인지를 확인하는 것은 쉽지 않은데요. https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379094672 에 그 내용이 적혀있습니다. 요약하자면 never
는 빈 유니온 타입이고, 타입스크립트는 조건부 타입 내부에 있는 유니온 타입을 자동으로 결정하는데, 이때 never
가 들어왔으므로 조건 타입이 never
가 됩니다.
따라서 never
임을 판단하려면 타입스크립트 공식 예제에서 제공하는 다음과 같은 코드로 판단할 수 있습니다.
type IsNever<T> = [T] extends [never] ? true : false
type Res1 = IsNever<never> // 'true' ✅
type Res2 = IsNever<number> // 'false' ✅
참고 자료
https://www.totaltypescript.com/as-never
https://ui.toast.com/posts/ko_20220323
https://witch.work/posts/typescript-never-type
https://www.zhenghao.io/posts/type-hierarchy-tree#the-bottom-of-the-tree