TypeScript의 템플릿 리터럴 타입: 타입 검사 중 파싱 및 활용 방법
TypeScript 템플릿 리터럴 타입: 기초 이론과 기본 활용
TypeScript는 JavaScript의 슈퍼셋으로, 정적 타입 시스템을 제공하여 코드의 안정성을 높여줍니다.
특히 템플릿 리터럴 타입(Template Literal Types)은 TypeScript 4.1에서 도입된 기능으로, 문자열 리터럴 타입을 더 유연하게 다룰 수 있게 해줍니다.
이번 포스트에서는 템플릿 리터럴 타입의 기본 개념과 간단한 활용법을 알아보겠습니다.
템플릿 리터럴 타입이란?
템플릿 리터럴 타입은 JavaScript의 템플릿 리터럴(백틱(`)을 사용한 문자열)과 비슷한 문법을 가지고 있지만, 타입 수준에서 동작합니다.
이를 통해 문자열 리터럴 타입을 조합하거나 변환할 수 있으며, 타입 시스템 내에서 문자열의 형식을 강제할 수 있습니다.
이 블로그 게시물에서 사용되는 표기법
타입 검증을 위해 ts-expect
라는 npm 패키지를 사용합니다. 이 패키지는 타입이 예상대로 동작하는지 검증하는 데 유용합니다.
// 값의 타입 검증
expectType<string>('abc'); // 'abc'는 string 타입입니다.
expectType<number>(123); // 123은 number 타입입니다.
// 타입의 동일성 검증
type Pair<T> = [T, T];
expectType<TypeEqual<
Pair<string>, [string, string]
>>(true); // Pair<string>은 [string, string]과 동일합니다.
템플릿 리터럴 타입의 기본 문법
템플릿 리터럴 타입은 백틱()을 사용하여 문자열 리터럴 타입을 정의합니다.
이 때 ${}` 안에 타입을 넣어 동적으로 문자열을 조합할 수 있습니다.
type Song<Num extends number, Bev extends string> =
`${Num} bottles of ${Bev}`;
위 코드에서 Song
타입은 두 개의 제네릭 매개변수(Num
과 Bev
)를 받아 문자열 리터럴 타입을 생성합니다.
예를 들어, Song<99, 'juice'>
는 '99 bottles of juice'
라는 문자열 리터럴 타입이 됩니다.
연결은 분배적입니다
템플릿 리터럴 타입은 문자열 리터럴 유니온 타입과 함께 사용될 때 분배적으로 동작합니다.
즉, 유니온 타입의 각 멤버에 대해 템플릿 리터럴 타입이 적용됩니다.
type Modules = 'fs' | 'os' | 'path';
type Prefixed = `node:${Modules}`;
expectType<TypeEqual<
Prefixed, 'node:fs' | 'node:os' | 'node:path'
>>(true);
위 예제에서 Prefixed
는 'node:fs'
, 'node:os'
, 'node:path'
의 유니온 타입이 됩니다.
하위 문자열 추출
템플릿 리터럴 타입에서 infer
키워드를 사용하면 문자열의 일부분을 추출할 수 있습니다.
이는 정규 표현식의 캡처 그룹과 유사한 기능입니다.
type ParseSemver<Str extends string> =
Str extends `${infer Major}.${infer Minor}.${infer Patch}`
? [Major, Minor, Patch]
: never;
expectType<TypeEqual<
ParseSemver<'1.2.3'>, ['1', '2', '3']
>>(true);
위 코드에서 ParseSemver
는 '1.2.3'
과 같은 문자열을 ['1', '2', '3']
으로 분해합니다.
문자열 값 제약
템플릿 리터럴 타입을 사용하면 특정 형식을 따르는 문자열 리터럴 타입을 정의할 수 있습니다.
예를 들어, 버전 문자열을 v숫자.숫자
형식으로 제약할 수 있습니다.
type Version = `v${number}.${number}`;
const version1: Version = 'v1.0'; // OK
const version2: Version = 'v2.zero'; // Error: 'zero'는 숫자가 아닙니다.
유틸리티 타입
TypeScript는 문자열 리터럴 타입을 조작하기 위한 네 가지 유틸리티 타입을 제공합니다.
- Uppercase: 문자열을 대문자로 변환
expectType<TypeEqual< Uppercase<'hello'>, 'HELLO' >>(true);
- Lowercase: 문자열을 소문자로 변환
expectType<TypeEqual< Lowercase<'HELLO'>, 'hello' >>(true);
- Capitalize: 문자열의 첫 글자를 대문자로 변환
expectType<TypeEqual< Capitalize<'hello'>, 'Hello' >>(true);
- Uncapitalize: 문자열의 첫 글자를 소문자로 변환
expectType<TypeEqual< Uncapitalize<'HELLO'>, 'hELLO' >>(true);
튜플 작업
템플릿 리터럴 타입은 튜플과 함께 사용될 때도 유용합니다.
예를 들어, 문자열 튜플을 하나의 문자열로 결합할 수 있습니다.
type Join<Strs extends string[], Sep extends string = ','> =
Strs extends [
infer First extends string,
...infer Rest extends string[]
]
? Rest['length'] extends 0
? First
: `${First}${Sep}${Join<Rest, Sep>}`
: '';
expectType<TypeEqual<
Join<['Hello', 'How', 'Are', 'You'], ' '>,
'Hello How Are You'
>>(true);
TypeScript 템플릿 리터럴 타입: 고급 활용과 실제 예제
이번에는 템플릿 리터럴 타입을 활용한 고급 기법과 실제 프로젝트에서의 활용 예제를 살펴보겠습니다.
템플릿 리터럴 타입은 문자열 리터럴을 타입 수준에서 다룰 수 있게 해주어, 복잡한 타입 시스템을 구축하는 데 매우 유용합니다.
객체 작업
템플릿 리터럴 타입은 객체의 프로퍼티 키를 동적으로 변환하거나 생성하는 데에도 사용할 수 있습니다.
예를 들어, 객체의 모든 키에 접두사를 추가하는 작업을 타입 수준에서 처리할 수 있습니다.
예: 속성 이름에 접두사 추가
type PrependDollarSign<Obj> = {
[Key in (keyof Obj & string) as `$${Key}`]: Obj[Key];
};
type Items = {
count: number,
item: string,
[Symbol.toStringTag]: string,
};
expectType<TypeEqual<
PrependDollarSign<Items>,
{
$count: number,
$item: string,
// 생략: [Symbol.toStringTag]
}
>>(true);
위 코드에서는 PrependDollarSign
타입이 객체의 모든 키에 $
접두사를 추가합니다.
Symbol.toStringTag
와 같은 심볼 키는 변환되지 않습니다.
실제 예제
템플릿 리터럴 타입은 다양한 실제 상황에서 유용하게 사용될 수 있습니다.
아래는 몇 가지 흥미로운 예제입니다.
예: 터미널에 출력 스타일 지정
Node.js에서 터미널에 스타일을 적용해 텍스트를 출력할 때, 템플릿 리터럴 타입을 사용해 스타일 문자열의 형식을 강제할 수 있습니다.
const styles = [
'bold',
'italic',
'underline',
'red',
'green',
'blue',
] as const;
type StyleUnion = typeof styles[number];
type StyleTextFormat =
| `${StyleUnion}`
| `${StyleUnion}+${StyleUnion}`
| `${StyleUnion}+${StyleUnion}+${StyleUnion}`;
function styleText(format: StyleTextFormat, text: string): string {
return util.styleText(format.split('+'), text);
}
styleText('bold+underline+red', 'Hello!'); // OK
styleText('bol+underline+red', 'Hello!'); // Error: 'bol'은 유효한 스타일이 아닙니다.
이 예제에서는 StyleTextFormat
타입을 사용해 스타일 문자열의 형식을 정적으로 검사합니다.
예: 속성 경로
객체의 중첩된 속성 경로를 타입으로 정의할 때 템플릿 리터럴 타입을 사용할 수 있습니다.
type PropType<T, Path extends string> =
Path extends keyof T
? T[Path]
: Path extends `${infer First}.${infer Rest}`
? First extends keyof T
? PropType<T[First], Rest>
: unknown
: unknown;
const obj = { a: { b: ['x', 'y'] } } as const;
expectType<
readonly ['x', 'y']
>(getPropValue(obj, 'a.b'));
expectType<
'y'
>(getPropValue(obj, 'a.b.1'));
위 코드에서 PropType
은 객체의 중첩된 속성 경로를 타입으로 추론합니다.
예: 속성 이름 접두사 변경
JSON-LD 형식의 객체에서 @
로 시작하는 키를 _
로 변경하는 작업을 타입 수준에서 처리할 수 있습니다.
type PropKeysAtToUnderscore<Obj> = {
[Key in keyof Obj as AtToUnderscore<Key>]: Obj[Key];
};
type AtToUnderscore<Key> =
Key extends `@${infer Rest}` ? `_${Rest}` : Key;
type JsonLd = {
'@context': string,
'@type': string,
datePublished: string,
};
expectType<TypeEqual<
PropKeysAtToUnderscore<JsonLd>,
{
_context: string,
_type: string,
datePublished: string,
}
>>(true);
예: 카멜 케이스를 하이픈 케이스로 변환
카멜 케이스(camelCase
)로 작성된 문자열을 하이픈 케이스(kebab-case
)로 변환하는 작업을 타입 수준에서 처리할 수 있습니다.
type ToHyphenCase<Str extends string> =
HyphenateWords<SplitCamelCase<Str>>;
type SplitCamelCase<Str extends string> =
Str extends `${infer First}${infer Rest}`
? IsUppercase<First> extends true
? [First, ...SplitCamelCase<Rest>]
: [`${First}`, ...SplitCamelCase<Rest>]
: [];
type HyphenateWords<Words extends string[]> =
Words extends [infer First, ...infer Rest]
? Rest['length'] extends 0
? Lowercase<First>
: `${Lowercase<First>}-${HyphenateWords<Rest>}`
: '';
expectType<TypeEqual<
ToHyphenCase<'howAreYou'>,
'how-are-you'
>>(true);
예: 하이픈 케이스를 카멜 케이스로 변환
반대로 하이픈 케이스를 카멜 케이스로 변환하는 작업도 가능합니다.
type ToLowerCamelCase<Str extends string> =
Uncapitalize<ToUpperCamelCase<Str>>;
type ToUpperCamelCase<Str extends string> =
camelizeWords<Split<Str, '-'>>;
type camelizeWords<Words extends string[]> =
Words extends [infer First, ...infer Rest]
? Rest['length'] extends 0
? Capitalize<First>
: `${Capitalize<First>}${camelizeWords<Rest>}`
: '';
expectType<TypeEqual<
ToLowerCamelCase<'how-are-you'>,
'howAreYou'
>>(true);
사람들이 템플릿 리터럴 타입으로 하는 멋진 작업
템플릿 리터럴 타입은 다양한 창의적인 방식으로 활용되고 있습니다. 몇 가지 예를 살펴보겠습니다.
- Node.js: UUID 타입 정의
type UUID = `${string}-${string}-${string}-${string}-${string}`;
- CLI 인자 파싱
const opts = program .option("-e, --episode <num>", "Download episode No. <num>") .option("--keep", "Keeps temporary files") .opts();
- DOM 요소 타입 추론
const a = querySelector('div.banner > a.call-to-action'); // HTMLAnchorElement
- Tailwind 색상 변형
type TailwindColor = `${BaseColor}-${Variant}`;
끝맺음
템플릿 리터럴 타입은 TypeScript에서 매우 강력한 도구이지만, 몇 가지 주의사항이 있습니다.
- 오류 메시지가 명확하지 않을 수 있습니다.
- 타입 수준의 코드는 이해하기 어려울 수 있습니다.
- 재귀를 사용할 경우 타입 검사 속도가 느려질 수 있습니다.
템플릿 리터럴 타입은 복잡한 문자열 리터럴 타입을 다루는 데 매우 유용하지만, 적절히 활용하는 것이 중요합니다.
'Javascript' 카테고리의 다른 글
PM2, 정말 괜찮은 걸까요? 2025년 Node.js 앱 관리, 메모리 누수 이슈와 대안 솔루션 총정리 (Kubernetes, Systemd) (0) | 2025.02.13 |
---|---|
TypeScript 튜플의 모든 것: 실전 예제로 풀어보는 타입 활용법 (1) | 2025.02.13 |
RegExp.escape() 마스터하기: 정규표현식 이스케이프 처리 완벽 가이드 (0) | 2025.02.13 |
리액트 폼 데이터 저장, 이제 Nuqs로 완벽하게 해결하세요! (0) | 2025.02.09 |
URL 쿼리 스트링과 React 리액트 상태 관리의 만남: `nuqs` 사용법 가이드 (0) | 2025.02.09 |