Typescript

[Typescript] Template Literal Types

prefer2 2022. 10. 2. 14:08

 

 

 

 

Template Literal Type?


Template literal types build on string literal types, and have the ability to expand into many strings via unions.

 

기존 TypeScript의 String Literal Type을 기반으로 새로운 타입만들기. 타입스크립트 4.1에 들어서 새롭게 도입되었다고 한다. 간단한 예시로 알아보자.

type World = "world";
 
type Greeting = `hello ${World}`;

기존의 World라는 타입을 기반으로 새로운 타입 Greeting을 쉽게 만들 수 있다. 

 

 

Union Type

type Citrus = "lemon" | "orange";
type Berry = "strawberry" | "blueberry";
 
type FruitIcecream = `${Citrus | Berry}_icecream`;
type FruitUnoin = `${Citrus}-${Berry}`;
          
// type FruitIcecream = "lemon_icecream" | "orange_icecream" | "strawberry_icecream | "blueberry_icecream
// type FruitUnoin = "lemon-strawberry" | "lemon-blueberry" | "orange-strawberry" | "orange-blueberry"

하나 또는 여러개의 Union Type을 합쳐서도 쓸 수 있다. 일반 string과도 합칠 수 있고 각각의 type끼리도 조합을 만들어서 쓸 수 있다. 원래라면 중복해서 FruitUnion type을 따로 정의해야겠지만 template literal type을 사용하면 중복 없이 간결하게 표현할 수 있다.

 

 

 

String Unions in Types


이미 알고 있는 정보들을 기반으로 새로운 string을 정의할때 사용하기에 좋다. 예시를 보자

// 이벤트 이름이 하나 추가될 때마다
type EventNames = 'click' | 'doubleClick' | 'mouseDown' | 'mouseUp';

type MyElement = {
    addEventListener(eventName: EventNames, handler: (e: Event) => void): void;

    // on~ 도 하나씩 추가해줘야 한다 😱
    onClick(e: Event): void;
    onDoubleClick(e: Event): void;
    onMouseDown(e: Event): void;
    onMouseUp(e: Event): void;
};

만약에 새로운 이벤트 focus를 추가하게 된다면 이를 EventNames에도 추가하고, MyElement내의 함수에도 추가로 onFocus를 추가해주어야한다. 특히 EventNames type이 여기저기서 쓰인다면 어디를 수정을 해주어야 하는 범위가 넒어질 것이다. 이는 매우 반복되고 헷갈리는 작업이다. 하지만 Template Literal Type을 사용하면 한 곳만 수정해도 모든 곳에 반영이 가능하다. 

 

type EventNames = 'click' | 'doubleClick' | 'mouseDown' | 'mouseUp';

type Handlers = {
  [H in EventNames as `on${Capitalize<H>}`]: (event: Event) => void;
};

// 원래 MyElement 그대로 작동한다
type MyElement = Handlers & {
  addEventListener: (eventName: EventNames, handler: (event: Event) => void) => void;
};

EventNames를 돌면서 첫글자를 대문자화하고 on을 붙여 Handler 타입을 쉽게 정의할 수 있다! 이제 EventNames만 수정하면 MyElement 에서 이벤트를 구독하는 양쪽 모두 대응이 되어 코드가 깔끔해지고 실수의 여지가 적어진다!

 

 

with conditional type

conditional type과 잘 엮어쓰면 여러가지 타입을 쉽게 만들 수 있다. 

type WeBareBears<T> = T extends `${infer R}Bear` ? R : never;

// type I = "Ice"
type I = WeBareBears<"IceBear">;  
// type O = "Panda"
type P = WeBareBears<"PandaBear">;

여기서 infer는 return type을 명시해주는 것이다. extends 안에서만 사용할 수 있다.

 

 

재미난 예시

type Split<S extends string> = 
  S extends `${infer T}.${infer U}` 
    ? [T, ...Split<U>] 
    : [S];

재귀적으로 돌면서 string의 점으로 나누어 타입을 만드는 타입이다. 예를들어 'hi.hello.world'라면 이를 ['hi', 'hello',  'world']로 나누어준다. 호오...

 

 

 

나는 어떻게 사용했는가


진행하고 있는 프로젝트에서 두 가지 타입을 합쳐서 하나의 타입으로 만드는 경우가 종종있었다. 특히 두가지의 Id값을 섞는 경우에는 이를 따로 직접 타이핑해주어야하는 불편함이 있다. 다음과 같이 직접 id나 name을 다시 선언한다.

 

이와 같은 경우에 따로 타입을 지정해서 사용한다면 훨씬 안전하게 사용할 수 있겠다는 생각이 들어 다음과 같은 타입을 만들었다. 원하는 prefix를 작성하여 prefix로 사용하고 키 값을 첫번째 문자만 대문자로 만들어 이어준다.

keyof의 결과값이 string, number, symbol이기 때문에 string으로 강제해주기 위해 string과 인터섹트하였다. 키 값을 변경시키 위해 as 키워드를 사용하여 변경해주었다.

PickWithPrefix<T, K extends keyof T & string, Prefix extends string> = {
  [P in K as `${Prefix}${Capitalize<P>}`]: T[P];
};

 

Template Literal Type을 처음 접해보았는데 이런 식으로 안전하게 타이핑하기 위해서 사용하기 좋은 것 같다! 상수나 parser를 만들 때 사용하면 좋지 않을까 싶다.

나름 naepeon-type을 npm에 출시했다. 생각보다 npm 출시가 쉬워서 놀랐다

https://www.npmjs.com/package/naepyeon-types

 

 

멋진 Template literal type을 사용한 예시들을 모아둔곳

https://github.com/ghoullier/awesome-template-literal-types

 

 

 

참고

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

https://toss.tech/article/template-literal-types

 

반응형