템플릿 리터럴 타입
자바스크립트에서 템플릿 리터럴이란 변수를 문자열 내에 삽입할 수 있도록 하는 문법을 지칭한다. 타입스크립트에서도 이와 유사하게 템플릿 리터럴 타입이 존재한다. 기존의 리터럴 타입 및 유니언 타입을 변수처럼 사용하여 새로운 문자열 타입을 구성하는 방식이다. 이를 통해 문자열을 결합하거나 변형한 새로운 타입을 정의할 수 있으며, 타입스크립트의 강력한 타입 시스템과 결합하여 타입 안전성을 유지하면서도 유연하게 문자열 기반 타입을 다룰 수 있다.
// 일반적인 템플릿 리터럴 타입의 사용법
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
// type Alignment =
// | "top-left" | "top-center" | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
type Alignment = `${VerticalAlignment}-${HorizontalAlignment}`;
기존에 나는 위와 같이 문자열 타입과 문자열 타입을 조합하는 방식으로 이 템플릿 리터럴 타입을 활용해왔다. 더 작은 타입이 더 큰 타입을 구성하므로 유지보수의 관점에서도 각각의 타입 아홉 개를 모두 선언해주는 것보다야 훨씬 나은 방식이라고 할 수 있겠다. 그런데 나는 최근에 Next.js의 Image 컴포넌트를 뜯어보다가 number 타입을 사용해 템플릿 리터럴 타입을 구성할 수 있다는 사실을 알게 되었다.
export declare const Image: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "height" | "width" | "loading" | "ref" | "alt" | "src" | "srcSet"> & {
src: string | import("../shared/lib/get-img-props").StaticImport;
alt: string;
width?: number | `${number}`;
height?: number | `${number}`;
이렇게 사용할 경우 사용자로부터 width={123} 뿐만 아니라 width="123"과 같은 props를 넣어줘도 문제 없이 작동하며, width="fxxk"과 같은 문자열을 props으로 사용하게 될 경우 타입 오류를 던지게 된다. 그리고 이를 조금 더 적극적으로 사용하면 아래와 같은 방식으로 입력 값이 특정 숫자 범위 안에 포함되는지를 확인할 수 있다.
// 숫자로 표현할 수 있는 타입의 제한
type Numeric = number | `${number}`;
// 재귀적으로 호출되며 다음 숫자를 찾아준다
type Increment<T extends number, Acc extends number[] = []> =
Acc['length'] extends T ? [...Acc, 0]['length'] : Increment<T, [...Acc, 0]>
// 재귀적으로 호출되며 Start부터 End까지 이어지는 number 타입의 유니언 타입을 리턴한다
type RangeHelper<Start extends number, End extends number, Acc extends number[] = [Start]> =
Start extends End
? [...Acc, End][number]
: RangeHelper<Increment<Start>, End, [...Acc, Increment<Start>]>;
// string 타입을 number 타입으로 변환시킴
type StringToNumber<T extends Numeric> =
T extends number
? T
: T extends `${infer R extends number}`
? R
: never;
// number 타입과 `${number}` 타입을 받아서, 해당 숫자가 해당 범위 내에 있는지를 확인하는 타입
type IsInRange<T extends Numeric, Min extends Numeric = 0, Max extends Numeric = 1000> =
T extends string
? StringToNumber<T> extends RangeHelper<StringToNumber<Min>, StringToNumber<Max>>
? `${T}`
: never
: T extends RangeHelper<StringToNumber<Min>, StringToNumber<Max>>
? T
: never
const number1 = "20"
const number2 = 200
// a = "20"
const a: IsInRange<typeof number1, "10", 934> = number1
// 'number' 형식은 'never' 형식에 할당할 수 없습니다.
const b: IsInRange<typeof number2, 400, "900"> = number2
+ 위에서 본 IsInRange 타입은 재귀 타입, 조건부 타입, 제네릭 타입, infer 타입, 템플릿 리터럴 타입 등을 모조리 가져다 만든 타입이다. 하지만 안타깝게도 IsInRange<"1432", 0, 10000>과 같이 너무 깊은 재귀 호출을 필요로 하는 타입은 마치 JS의 스택오버플로우 같은 현상을 일으킨다. 확인해본 결과로는 1000번보다 더 깊게 호출되는 경우 TS엔진이 추론을 포기하고 대신 any로 추론하는 듯하다. 아쉽지만 IsInRange 타입은 좁은 범위 내에서만 사용해야겠다.
블로그의 정보
Ayden's journal
Beard Weard Ayden