[ Language > Typescript ]
[Typescript] Template Literal Types
[Typescript] Template Literal Types
Typescript 기능들을 하나씩 다뤄볼 예정입니다. 그 첫 번째 기능으로 당첨된 것은 Template Literal Types입니다. literal types 자체는 어려운 내용이 아니지만, 2.활용 예시는 Generic Type과 Type infer 대해 알고 있어야 이해가 가능할 것 같습니다.
1. Literal 타입과 Template Literal 타입
Template Literal 타입이란 문자열 리터럴을 템플릿 문자열 형식(백틱 ``)으로 만들어 조합하여 사용할 수 있게 해주는 타입입니다.
Template Literal 타입과 String Literal 타입은 문자열 규칙에 따른 디자인 토큰 생성이나, Object의 key값에 따른 자동 타입 생성같이 다양한 상황에서 사용할 수 있습니다. 아래 각 타입에 대한 설명입니다.
Literal Type : string, number 같이 일반적인 타입만이 아니라, 특정 문자열이나 숫자를 타입으로 지정한 타입입니다.
type Direction = "left" | "right";const direction:Direction = "left"; ✅const direction:Direction = "up"; ❌ // left와 right 값만 가질 수 있다.Type '"up"' is not assignable to type '"left" | "right"'const direction:string = "up"; ✅ // string 타입은 모든 문자열이 가능하다.
Template Literal : String Literal을 템플릿 문자열 형식(백틱 ``)으로 조합하여 만드는 타입입니다.
type Fruit = "apple" | "banana" | "mango";type Sentence = `I like ${Fruit}`;const sentence:Sentence = "I like apple"; ✅const sentence:Sentence = "I hate apple"; ❌ // I like 가 아니다.Type '"I hate apple"' is not assignable to type '"I like apple" | "I like banana" | "I like mango"'.const sentence:Sentence = "I like melon"; ❌ // Fruit 타입에 없는 과일이다.Type '"I like melon"' is not assignable to type '"I like apple" | "I like banana" | "I like mango"'.
2. Template Literal 활용 예시
Template Literal을 어느 상황에 사용할 수 있는지 예시로 보여드리겠습니다. 저가 Template Literal을 사용했던 예시 1가지와, 공식 문서의 활용 예시 1가지를 가져와 봤습니다.
2.1 디자인 토큰 - Typography
저는 이 기능을 디자인 시스템에서 {type}-{Size}-{Weight}의 규칙을 가진 Typography 디자인 토큰을 정의하는데 활용했었습니다. 아래 그 예시 코드입니다.
type FontType = "title" | "body";type FontSize = "sm" | "md" | "lg";type FontWeight = "regular" | "medium" | "bold";type FontVariant = `${FontType}-${FontSize}-${FontWeight}`; // FontType 2가지 FontSize 3가지 * FontWeight 3가지 = 18가지;// "title-sm-regular", "title-md-regular", "title-md-bold", "title-md-regular...등등 18가지 문자열 유니온 타입이 만들어집니다.type FontProperties = Pick< CSSProperties, "fontWeight" | "lineHeight" | "fontSize" | "letterSpacing">;type Typography = Record<FontVariant, FontProperties>;const typography: Typography = { "body-lg-bold": { fontSize: "1rem", fontWeight: 700, letterSpacing: 0, lineHeight: 1, }, "body-lg-medium": { fontSize: "1rem", fontWeight: 500, letterSpacing: 0, lineHeight: 1, }, //...};
만약 Typography 타입에 대해 더 타이트하게 적용하고 싶다면 응용할 수 있습니다. 다음은 FontWeight에 따라 자동으로 CSS fontWeight 타입이 고정되는 예시입니다.
import {CSSProperties} from "react";type FontType = "title" | "body";type FontSize = "sm" | "md" | "lg";type FontWeightValueMap = { regular: 400; medium: 500; bold: 700;};// 만약 변수로도 사용하고 싶으면 const fontWeightValueMap으로 변수로 만들어 사용. typeof 지시어로 타입 호출.type FontWeight = keyof FontWeightValueMap; // "regular" | "medium" | "bold"type FontWeightCSSProperty<FW extends FontWeight> = { fontWeight: FontWeightValueMap[FW];}; //FontWeight에 따라 값 CSS literal type number로 고정.type FontVariant = `${FontType}-${FontSize}-${FontWeight}`; // 18가지 토큰type ExtractFontWeight<FV extends FontVariant> = FV extends `${string}-${string}-${infer FW}` ? FW : never; // FontVariant에서 FontWeight 추출type FontProperties<FV extends FontVariant> = Pick< CSSProperties, "lineHeight" | "fontSize" | "letterSpacing"> & FontWeightCSSProperty<ExtractFontWeight<FV>>; //fontWeight는 Font Variant의 Font Weight에 따라 값 결정.type Typography = { [key in FontVariant]: FontProperties<key>;};const typography: Typography = { "body-lg-bold": { fontSize: "1rem", fontWeight: 700, ✅ letterSpacing: 0, lineHeight: 1, }, "body-lg-medium": { fontSize: "1rem", fontWeight: 700, ❌ medium은 500의 값을 가져야 하기 때문에 타입 에러.Type '700' is not assignable to type '500' letterSpacing: 0, lineHeight: 1, }, //...};
디자인 시스템에서 두 가지 타입 설정 방법 중에 뭐가 더 낫냐고 물으신다면 어떤 이점을 가지고 싶은 지 혹은 상황에 따라 다릅니다. 디자인 토큰 구조를 더 이상 건드릴 예정도 없거나 빠르게 끝낼 프로젝트라면 타입에 신경 쓰는 것은 시간 낭비 밖에 안됩니다. 하지만 중요하게 쓸 디자인 시스템으로 피그마까지 연동하여 버전 관리과 자동 Figma 반영까지 고려했다면 명확한 디자인 기획에 따라 타입을 설정해주는 것이 더 좋아 보입니다. 결론은 저의 생각으로는 가용 인력, 숙련도, 여유 시간 등 상황에 따라 정하는게 좋아 보입니다.
2.2 공식 문서 예시
아래는 공식문서의 object의 literal 키 값과 그 value의 타입에 따라 함수의 타입이 자동으로 만들어지는 예시입니다. object의 value 타입이 다양하더라도 개발자는 편하게 매칭된 타입으로 코딩을 할 수 있습니다.
type PropEventSource<T> = { on<Key extends string & keyof T> (eventName: `${Key}Changed`, callback: (newValue: T[Key]) => void): void;};// ${T의 key 값} + "Changed"가 eventName이 된다. callback의 newValue는 ${T의 key 값에 매칭되는 value의 타입이된다}/// Create a "watched object" with an `on` method/// so that you can watch for changes to properties.declare function makeWatchedObject<T>(obj: T): Type & PropEventSource<T>;const person = makeWatchedObject({ firstName: "Saoirse", lastName: "Ronan", age: 26});person.on("firstNameChanged", (newName) => {console.log(newName)});// on함수의 1번째 파라미터는 `{key}Changed`이 되고, 2번째 파라미터 함수의 newName은 자동으로 string 타입이 된다. person.on("ageChanged", newAge => {console.log(newAge)})// 마찬가지로 ${person의 key 값 = "age"}+"Changed", newAge는 person["age"]의 타입이다.
3. 관련 유틸리티 타입
아래는 Template Literal, String Literal과 관련된 유틸리티 타입들 입니다. 공식 문서에서 가져왔습니다.
3.1 Uppercase
모든 글자를 대문자인 타입으로 변환해주는 유틸리티.
type Greeting = "Hello, world"type ShoutyGreeting = Uppercase<Greeting> // "HELLO, WORLD" 타입type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`type MainID = ASCIICacheKey<"my_app"> // "ID-MY_APP" 타입
3.2 Lowercase
모두 글자를 소문자인 타입으로 변환해주는 유틸리티.
type Greeting = "Hello, world"type QuietGreeting = Lowercase<Greeting> // "hello, world" 타입type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`type MainID = ASCIICacheKey<"MY_APP"> // "id-my_app" 타입
3.3 Capitalize
첫 글자를 대문자인 타입으로 변환해주는 유틸리티.
type Greeting = "hello, world"type ShoutyGreeting = Uppercase<Greeting> // "Hello, world" 타입
3.4 Uncapitalize
첫 글자를 소문자인 타입으로 변환해주는 유틸리티
type Greeting = "HELLO WORLD"type ShoutyGreeting = Uppercase<Greeting> // "hELLO WORLD" 타입