타입스크립트의 템플릿 리터럴

타입스크립트 4.1에 도입된 템플릿 리터럴을 통해 대수적 타입과 문제해결 능력에 대해 고민해봅니다

타입스크립트의 템플릿 리터럴

목표

 타입스크립트 4.1에 도입된 템플릿 리터럴 타입을 알아보고 대수적 타입(Algebraic Data Types, 이후 ADTs)이 왜 정적 타입 언어에 필요한지 이해합니다.

 정적 타입 언어는 런타임 전에 타입을 체크함으로 에러를 미리 발견합니다.
 완벽한 언어는 없습니다. 대부분 정적 타입 언어의 한계점은 ad-hoc 타입 시스템입니다. ad-hoc은 "임시적인, 전용"이라는 뜻입니다. 예를 들어 유저 데이터의 타입을 다루고 싶을 때 우리는 User라는 전용 타입을 만들고 관리합니다.
 만약 아무 곳에나 써도 호환이 되는 만능 타입이 존재한다고 가정해봅시다. 하나의 타입으로 유저, 게임, 음식, 애완동물, 게시물 등 모든 데이터를 다룬다면 서로 어떻게 구분할 수 있을까요? ad-hoc 타입 시스템은 피할 수 없는 운명입니다.

당연한 듯 보입니다. 이게 왜 문제일까요?

 이러한 시스템은 고도의 추상화와 범위 모델링을 어렵게 만들었습니다. 타입이 예측에서 조금이라도 벗어날 것 같으면 정지부터 하는 예민한 시스템은 멋지지 않습니다. 제네릭, 유니온, 튜플이 없는 타입스크립트를 생각해보세요.
 이를 보완하는 연구와 개선이 많습니다. Martin-Löf Type Theory, Generic, C++의 Dependent Types, Rust의 대수적 타입이 좋은 예시입니다.
 대수적 타입은 타입을 수학처럼 더하고 곱해서 문제 해결, 대응에 도움되는 유연한 타입을 만들자는 아이디어입니다. 더하는 타입과 곱하는 타입이 있습니다.

// 더하는 타입
type Direction = "up" | "down" | "right" | "left"
type Params = string | number
// 곱하는 타입
type Person = { name: string, age: number }
type Tuple = readonly [boolean, number, string]

 더하는 타입은 여러 선택지가 있는 타입이고, 곱하는 타입은 하나 안에 여러 타입이 공존합니다. 그게 전부입니다. 지극히 간단한 원리입니다.

템플릿 리터럴 타입?

 템플릿 리터럴 타입은 대수적 타입의 범위 모델링을 강력하게 지원합니다. 템플릿 리터럴 타입이 도입되기 전 코드를 봅시다.

// case 1: 버튼 타입이 추가될 때마다...
type Buttons = "a" | "b" | "x" | "y" | "home" | "zl" | "zr";

// 💔 버튼에 대한 메소드 타입도 일일이 추가해야 한다.
type ButtonsController = {
  onA: () => void;
  onB: () => void;
  onX: () => void;
  onY: () => void;
  onHome: () => void;
  onZl: () => void;
  onZr: () => void;
};

class Controller implements ButtonsController {
  // ... 메소드 구현
}

// case 2: 포커용 카드 타입을 어떻게 만들지?
type CardRank = 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "J" | "Q" | "K" | "A";
type CardSuit = "♥" | "♠" | "♣" | "◆";
type Card = [CardSuit, CardRank] | "JOKER"; // 🙇‍♂️ 흠...뭔가 별로다.

 case 1: 타입이 추가되거나 타입 이름이 변경될 때마다 메소드 타입을 정의하는 코드도 매번 고쳐야 합니다. 번거로울 뿐더러 실수할 수도 있습니다.
 case 2: 튜플 | "JOKER" 타입이라 통일감이 떨어지고 보기 안 좋습니다.

 상술했던 ad-hoc 타입 시스템의 단점을 명확히 보여줍니다. 타입이 너무 구체적이면 바꾸려고 할 때마다 관련된 것들을 일일이 고쳐야 하고, 복잡한 타입에 적용하기도 힘듭니다.

 템플릿 리터럴 타입으로 위 코드를 개선해봅시다.

// case 1: 버튼에 대한 타입만 추가하거나 변경합니다.
type Buttons = "a" | "b" | "x" | "y" | "home" | "zl" | "zr" 
type CapitalizedButtons = Capitalize<Buttons> // 📘 Capitalize: TS 4.1에 추가된 타입. string 리터럴 타입의 첫 글자를 대문자로 바꿉니다.
type ButtonHandlers = `on${CapitalizedButtons}` // "onA" | "onB" | "onX" | "onY" | "onHome" | "onZl" | "onZr"

// 🎉 메소드 타입은 버튼 타입에 맞춰서 자동으로 업데이트됩니다!
type ButtonsController = {
  [BH in ButtonHandlers]: () => void;
}

class Controller implements ButtonsController {
  // ... 메소드 구현
}

// case 2: 포커 카드
type CardRank = 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "J" | "Q" | "K" | "A";
type CardSuit = "♥" | "♠" | "♣" | "◆";
type Card = `${CardSuit}-${CardRank}` | "JOKER"; // 53가지의 문자열 리터럴 타입 생성

 타입을 좀 더 유연하게 선언할 수 있고, 약간의 수정 작업도 넣을 수 있습니다. 이 덕분에 기존에는 할 수 없거나, 번거로웠던 타입 작업을 훨씬 간편하게 구현할 수 있습니다.

마무리

 대수적 타입을 통한 사고방식과 방법은 문제해결에 도움을 줍니다. 템플릿 리터럴 타입으로 강화된 타입스크립트를 즐겨보세요!