Search
😋

TypeScript의 Intrinsic String Manipulation Types 사용 예시

Date
2021/09/11
Tags
TypeScript
Created by
Uppercase<T>, Lowercase<T> 이런 거, 대체 왜 존재하는 거야?
Table of Contents

Template Literal Types

Template literal types는 TypeScript 4.1에서 추가되었다.
type World = "world"; // type Greeting = "hello world" type Greeting = `hello ${World}`;
TypeScript
Value expression에서 사용하던 Template literal string 문법(`backtick으로 감싸진 ${expression}을 이렇게 사용할 수 있는 문자열`)을 Type expression에서도 사용할 수 있는 것이다. 당연히 새로운 값을 만들어내는 것이 아닌, 새로운 타입을 만들어낸다.
type ArtFeatures = "cabin" | "tree" | "sunset" type Colors = | "darkSienna" | "sapGreen" | "titaniumWhite" | "prussianBlue" // type ArtMethodNames = "paint_darkSienna_cabin" | "paint_darkSienna_tree" | "paint_darkSienna_sunset" | ... type ArtMethodNames = `paint_${Colors}_${ArtFeatures}`
TypeScript
Template literal types는 string union 타입과 함께할 때 빛을 발한다. Template literal types에 string union 타입을 사용하면, 사용된 string union 타입들의 모든 가능한 조합을 나타내는 새로운 타입을 반환한다.

Intrinsic String Manipulation Types

TypeScript 4.1에서는 위에서 설명한 Template literal types와 더불어 Intrinsic string manipulation types가 추가되었다. 이 제네릭 타입들은 string type을 더 편리하게 다루기 위해 추가되었으며, 성능을 위해 TS 컴파일러에 빌트인되어 있고 .d.ts에서 찾아볼 수 없다.
실제로 VSCode 등에서 제네릭 타입 위에 hover해봐도 intrinsic이라고 표시될 뿐 어떻게 구현되어 있는지는 표시되지 않는다. TS 컴파일러 내 구현은 다음과 같으며, 평범하게 JavaScript string 함수들을 사용하는 것을 알 수 있다.
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; }
TypeScript
제공하는 제네릭 타입들과 사용 예시는 다음과 같다. 이름과 사용 예시만 봐도 너무 쉽게 무엇을 하는지 알 수 있는 제네릭들이라 설명은 생략한다.

Uppercase<StringType>

type Greeting = "Hello, world" // type ShoutyGreeting = "HELLO, WORLD" type ShoutyGreeting = Uppercase<Greeting>
TypeScript

Lowercase<StringType>

type Greeting = "Hello, world" // type QuietGreeting = "hello, world" type QuietGreeting = Lowercase<Greeting>
TypeScript

Capitalize<StringType>

type LowercaseGreeting = "hello, world"; // type Greeting = "Hello, world" type Greeting = Capitalize<LowercaseGreeting>;
TypeScript

Uncapitalize<StringType>

type UppercaseGreeting = "HELLO WORLD"; // type UncomfortableGreeting = "hELLO WORLD" type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
TypeScript

둘을 조합해서 사용하는 예시

여기까지 읽고 나면 "아니 그래서 Template literal types로 union types를 쉽게 확장시킬 수 있는 건 알겠는데, Uppercase<T>, Lowercase<T>, Capitalize<T>, Uncapitalize<T> 이런 타입들은 왜 존재하는 거야?"라는 의문이 들 수 있다. 이들 중 Capitalize<T>를 사용하는 예시를 직접 살펴보자.
어떤 특정한 데이터를 다루는 레거시 SDK의 타입을 정의해야 한다고 생각해 보자.
interface Data { digits: number[] names: string[] flags: Record<"darkMode" | "mobile", boolean> }
TypeScript
데이터 타입이 위와 같다고 하면, SDK에는 setDigits(), getDigits(), setNames() 등 수많은 getter와 setter가 구현되어 있을 것이다. 이 모든 메소드들의 타입을 직접 정의하지 않고, Data 타입을 이용해 정의할 수 있을까?
조금만 생각해보면 keyof DataData의 프로퍼티를 가져와서 어떻게 하면 될 것 같다는 생각이 든다. Template literal types로 이들 앞에 모두 get, set을 붙이면 어떻게 될까?
type DataGetter = { [K in keyof Data as `get${K}`]: () => Data[K] } type DataSetter = { [K in keyof Data as `set${K}`]: (arg: Data[K]) => void } /* type DataGetter = { getdigits: () => number[]; getnames: () => string[]; getflags: () => Record<"darkMode" | "mobile", boolean>; } type DataSetter = { setdigits: (arg: number[]) => void; setnames: (arg: string[]) => void; setflags: (arg: Record<"darkMode" | "mobile", boolean>) => void; } */
TypeScript
의도하던 바는 어느 정도 만족했지만, 우리가 생각한 건 이게 아니다. 보통 getter, setter 메소드는 camelCase로 작성되어 있다.
자, 여기서 Capitalize<T>가 등장한다면 어떨까?
type DataGetter = { [K in keyof Data as `get${Capitalize<K>}`]: () => Data[K] } type DataSetter = { [K in keyof Data as `set${Capitalize<K>}`]: (arg: Data[K]) => void } /* type DataGetter = { getDigits: () => number[]; getNames: () => string[]; getFlags: () => Record<"darkMode" | "mobile", boolean>; } type DataSetter = { setDigits: (arg: number[]) => void; setNames: (arg: string[]) => void; setFlags: (arg: Record<"darkMode" | "mobile", boolean>) => void; } */
TypeScript
와! 순식간에 getter와 setter 타입 정의가 만들어졌다.
type DataSDK = DataGetter & DataSetter; function load(dataSDK: DataSDK) { dataSDK.setDigits([14]); dataSDK.setFlags({ darkMode: true, mobile: false }); return { digits: dataSDK.getDigits(), names: dataSDK.getNames(), flags: dataSDK.getFlags(), } as Data; }
TypeScript
이제 이렇게 DataSDK 타입을 만들어서 레거시 SDK를 지지고 볶고 갖고 놀 수 있다.
이런 식으로, Capitalize<T>를 데이터의 각 프로퍼티 이름을 기반으로 메소드의 타입을 정의하는 데에 사용할 수 있다.

Reference