템플릿 리터럴 타입
템플릿 리터럴 타입 (Template Literal Types)
템플릿 리터럴 타입은 문자열 리터럴 타입(string literal types)을 기반으로 하며, 유니온을 통해 여러 문자열로 확장될 수 있는 능력을 갖고 있어요.
자바스크립트의 템플릿 리터럴 문자열과 같은 구문이지만, 타입 위치에서 사용됩니다.
구체적인 리터럴 타입과 함께 쓰이면, 템플릿 리터럴은 내용을 이어 붙여 새 문자열 리터럴 타입을 만들어 냅니다.
type World = "world";
type Greeting = `hello ${World}`;
// ^?
보간(interpolation)되는 위치에 유니온이 사용되면, 그 타입은 각 유니온 멤버가 나타낼 수 있는 모든 가능한 문자열 리터럴의 집합이 돼요.
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// ^?
템플릿 리터럴의 보간 위치마다 유니온들은 서로 곱해져요(cross multiply).
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
// ---cut---
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// ^?
큰 문자열 유니온에는 미리 생성(ahead-of-time generation)을 사용하는 걸 일반적으로 권장하지만, 작은 경우에는 이 방식이 유용해요.
타입 안의 문자열 유니온 (String Unions in Types)
템플릿 리터럴의 힘은 타입 안의 정보를 바탕으로 새 문자열을 정의할 때 발휘돼요.
makeWatchedObject라는 함수가 전달받은 객체에 on()이라는 새 함수를 추가하는 경우를 생각해 봅시다. 자바스크립트에서 그 호출은 makeWatchedObject(baseObject)처럼 생겼을 거예요. base 객체를 이렇게 생겼다고 상상해 볼게요.
// @noErrors
const passedObject = {
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
};
base 객체에 추가될 on 함수는 두 개의 인자를 기대해요. eventName(a string)과 callback(a function)이죠.
eventName은 attributeInThePassedObject + "Changed" 형태여야 해요. 그래서 base 객체의 속성 firstName에서 파생된 firstNameChanged가 되는 거죠.
callback 함수는 호출될 때,
attributeInThePassedObject라는 이름과 연관된 타입의 값을 전달받아야 해요.firstName이string으로 타입되어 있으므로,firstNameChanged이벤트의 콜백은 호출 시점에string이 전달되길 기대하죠. 마찬가지로age와 연관된 이벤트는number인자와 함께 호출되길 기대해요.void반환 타입을 가져야 해요 (설명을 단순하게 하기 위해서요).
그러면 on()의 순진한(naive) 함수 시그니처는 on(eventName: string, callback: (newValue: any) => void)가 될 수 있어요. 하지만 위 설명에서 우리는 코드에 문서화하고 싶은 중요한 타입 제약을 확인했다. 템플릿 리터럴 타입을 사용하면 이런 제약을 코드 안으로 가져올 수 있어요.
// @noErrors
declare function makeWatchedObject(obj: any): any;
// ---cut---
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
// makeWatchedObject has added `on` to the anonymous Object
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});
on이 단지 "firstName"이 아니라 "firstNameChanged"라는 이벤트를 듣고 있음을 눈여겨보세요. 만약 적격한(eligible) 이벤트 이름의 집합이 watched 객체의 속성 이름 유니온에 "Changed"를 덧붙인 것으로 제약되도록 한다면, 우리의 순진한 on() 명세를 더 견고하게 만들 수 있어요. 자바스크립트에서 Object.keys(passedObject).map(x => ${x}Changed)처럼 그런 계산을 하는 것엔 익숙하지만, 템플릿 리터럴은 타입 시스템 안에서 문자열 조작에 비슷한 접근을 제공해요.
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
/// Create a "watched object" with an `on` method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
이렇게 하면, 잘못된 프로퍼티가 주어졌을 때 에러를 내는 무언가를 만들 수 있어요.
// @errors: 2345
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
// ---cut---
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
// Prevent easy human error (using the key instead of the event name)
person.on("firstName", () => {});
// It's typo-resistant
person.on("frstNameChanged", () => {});
템플릿 리터럴을 사용한 추론 (Inference with Template Literals)
우리는 전달된 원본 객체에 담긴 모든 정보를 활용하지 못했다는 점을 눈여겨보세요. firstName의 변경(즉 firstNameChanged 이벤트)이 주어졌을 때, 콜백이 string 타입의 인자를 받을 것이라 기대해야 해요. 마찬가지로 age의 변경에 대한 콜백은 number 인자를 받아야 하죠. 우리는 콜백의 인자를 any로 타입하는 순진한 방식을 쓰고 있어요. 여기서도 템플릿 리터럴 타입이, 속성의 데이터 타입이 그 속성의 콜백의 첫 인자와 같은 타입임을 보장하는 것을 가능하게 해줘요.
이를 가능하게 하는 핵심 통찰은, 다음과 같은 제네릭이 있는 함수를 사용할 수 있다는 거예요.
- 첫 인자에 사용된 리터럴이 리터럴 타입으로 포착된다
- 그 리터럴 타입이 제네릭에 있는 유효한 속성들의 유니온에 속하는지 검증될 수 있다
- 검증된 속성의 타입이 인덱스 접근(Indexed Access)을 통해 제네릭의 구조에서 조회될 수 있다
- 이 타입 정보는 그다음에 콜백 함수의 인자가 같은 타입이 되도록 보장하는 데 적용될 수 있다
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
// ^?
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
// ^?
if (newAge < 0) {
console.warn("warning! negative age");
}
})
여기서 on을 제네릭 메서드로 만들었어요.
사용자가 "firstNameChanged"라는 문자열로 호출하면, TypeScript는 Key에 대한 올바른 타입을 추론하려고 시도해요. 그렇게 하기 위해 Key를 "Changed" 앞의 내용과 대조해 "firstName"이라는 문자열을 추론합니다. TypeScript가 그것을 알아내면, on 메서드는 원본 객체에서 firstName의 타입을 가져올 수 있어요. 이 경우에는 string이죠. 마찬가지로 "ageChanged"로 호출하면 TypeScript는 age 프로퍼티의 타입인 number를 찾습니다.
추론은 다양한 방식으로 결합될 수 있어요. 종종 문자열을 분해하고 그것을 다른 방식으로 재구성하는 데 쓰이죠.
내장 문자열 조작 타입 (Intrinsic String Manipulation Types)
문자열 조작을 돕기 위해 TypeScript에는 문자열 조작에 쓰일 수 있는 타입 집합이 포함되어 있어요. 이 타입들은 성능을 위해 컴파일러에 내장되어 있으며, TypeScript에 포함된 .d.ts 파일에서는 찾을 수 없어요.
Uppercase<StringType>
문자열의 각 문자를 대문자 버전으로 변환합니다.
Example
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
// ^?
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
// ^?
Lowercase<StringType>
문자열의 각 문자를 소문자 버전으로 변환합니다.
Example
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
// ^?
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
// ^?
Capitalize<StringType>
문자열의 첫 문자를 대문자 버전으로 변환합니다.
Example
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// ^?
Uncapitalize<StringType>
문자열의 첫 문자를 소문자 버전으로 변환합니다.
Example
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// ^?
내장 문자열 조작 타입의 기술적 세부사항
TypeScript 4.1 기준으로, 이 내장 함수들을 위한 코드는 자바스크립트 문자열 런타임 함수를 직접 사용해 조작하며 로케일(locale)을 인식하지 않습니다.
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}
더 알아보기 (Learn more)
- Everyday Types의 문자열 리터럴 타입을 먼저 복습해 보세요.
- Mapped Types에서 템플릿 리터럴 타입을 매핑 타입과 함께 쓰는 법을 확인해 보세요.
- Indexed Access Types에서
Type[Key]문법을 더 알아보세요.