타입스크립트 4.x 버전 덕택에 지금까지 불가능했던 타입을 표현할 수 있게 되었다.
만약 타입만으로 코드 실행 결과의 검증이 가능하다면 1+2같은 함수의 테스트는 구현할 필요가 없을것이다.
즉 Add<3,5> 타입의 결과가 8이라면, Add<3,5> 타입은 8이외의 값은 허용하지 않을것이다.
타입스크립트의 타입만으로 컴파일 타임 이전에 알 수 있는 코드 실행 결과를 검증해보자.
사용하는 기능
이전부터 존재한 친숙한 기능들
- 조건부 타입 (conditional types)
- 3항 연산자
- SomeType === AnotherType ? TrueType : FalseType
- SomeType extends AnotherType ? TrueType : FalseType
- (주 : 정말 정확한 타입을 원한다면 extends를 양방향으로 먹여주면 된다 - AnotherType extends SomeType)
- 타입 추론 (type inference)
- infer 부분을 지역변수처럼 사용함. 지역변수에 타입을 할당한다고 생각하자.
- 조건부 타입과 함께 활용되여, 객체 혹은 함수의 내부 (객체 속성, 배열의 값, 함수의 파라미터, 리턴타입)의 타입을 구조분해 할당한다고 생각하면 좋다.
- type Flatten<T> = T extends (infer U)[] ? U : T
- Flatten<[number, string, object]>이면 결과는 (number | string | object)
- 제네릭 제약조건 (generic constraints)
- 제네릭이 인스턴스화 되길 기대하는 타입을 지정하면서, 제네릭 타입을 만드는 방법.
- 타입 변수에 해당하는 타입의 인스턴스가 해당 타입이 지정하는 값에 들어오길 기대
- type Flatten<T>은 T에 모든 타입의 가능성을 열어둠
- type Flatten<T extends any[]>는 T가 배열에 할당 가능해야 함을 의미.
4.x 이후에 추가된 기능들은 무엇들이 있을까요?
- 조건부 타입 재귀(recursion in conditional types)
- 조건부 타입의 분기 중 하나가 자신을 참조하고 조건부 타입의 논리를 통해 임의의 횟수만큼 재귀하도록 허용합니다.
- 4.1 이전 버전에서는 타입 재귀를 사용하려 하면 순환 참조 오류가 발생하였음
- 가변 길이 튜플(variadic tuples)
- 튜플 타입 내부에서 spread 연산자를 자유롭게 사용하여, 튜플 타입들을 결합할 수 있습니다.
- type Booleans = [boolean, boolean]
- type Derived = [...Booleans, string] = [boolean, boolean, string]
- 튜플 타입 내부에서 spread 연산자를 자유롭게 사용하여, 튜플 타입들을 결합할 수 있습니다.
- 탬플릿 리터럴 타입(template literal types)
- 템플릿 리터럴 구문을 사용하여 다른 문자열 리터럴 타입에서 문자열 리터럴 타입을 형성합니다.
- type ClickEvent = 'click' 을 취하여, 이벤트 핸들러 타입 type ClickEventHandler = `on${ClickEvent}`('onclick')를 파생시킬 수 있습니다.
- 템플릿 리터럴 구문을 사용하여 다른 문자열 리터럴 타입에서 문자열 리터럴 타입을 형성합니다.
타입 구현 방식
- 각 숫자 리터럴을 해당 튜플의 길이에 매핑합니다.
- 가변 길이 튜플 기능을 사용하여 튜플을 조작합니다.
- 결과 튜플의 길이를 숫자 리터럴에 다시 매핑합니다.
산술 계산의 기반이 튜플 길이이기 때문에
- 자연수가 아니면 대응이 불가능합니다.
- 음수, 분수 개념이 없습니다.
구현하기
기본 유틸리티
구현방식의 3번 : 배열(튜플)도 객체이며, 배열(튜플)의 length 속성의 타입(리터럴)을 L에 구조분해 할당한다고 생각합시다.
type Length<T extends any[]> =
T extends { length: infer L } ? L : never;
구현 방식의 1번 : 튜플을 숫자로 다시 매핑하기 위해 해당 타입을 배열에 할당할 수 있고 length 속성을 갖고 있다는 사실을 이용합니다.
type BuildTuple<L extends number, T extends any[] = []> =
T extends { length: L } ? T : BuildTuple<L, [...T, any]>;
let length: Length<[number, string, string, boolean]>; // `4`
let tuple: BuildTuple<5>; // `[any, any, any, any, any]`
기본 산술 구현
첫 번째 산술 타입인 더하기 및 빼기를 작성할 수 있습니다.
type Add<A extends number, B extends number> =
Length<[...BuildTuple<A>, ...BuildTuple<B>]>;
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...(infer U), ...BuildTuple<B>]
? Length<U>
: never;
Add는 각각 입력 중 하나 길이인 두 개의 튜플을 만들고 결합하여 결과 튜플의 길이를 결정하는 간단한 타입입니다.
Subtract는, A가 3이면, ...BuildTuple<B> 부분이 길이 2가 되므로, U는 BuildTuple<1>이 되어 [any] => Length<[any]> => 1이 됩니다.
Subtract가 제대로 동작하려면 A가 B보다 크거나 같아야 합니다.
type a =Subtract<0,0> // 0
let five: Add<3, 2>; // `5`
let one: Subtract<3, 2>; // `1`
좀 더 복잡한 유틸리티 타입
type MultiAdd<
N extends number, A extends number, I extends number
> = I extends 0 ? A : MultiAdd<N, Add<N, A>, Subtract<I, 1>>;
type b = MultiAdd<8,0,3> // 24
type EQ<A, B> =
A extends B
? (B extends A ? true : false)
: false;
type AtTerminus<A extends number, B extends number> =
A extends 0
? true
: (B extends 0 ? true : false);
type LT<A extends number, B extends number> =
AtTerminus<A, B> extends true
? EQ<A, B> extends true
? false
: (A extends 0 ? true : false)
: LT<Subtract<A, 1>, Subtract<B, 1>>;
type MultiSub<
N extends number, D extends number, Q extends number
> = LT<N, D> extends true
? Q
: MultiSub<Subtract<N, D>, D, Add<Q, 1>>;
분해해서 알아봅시다.
type c = MultiSub<8,3,0> // 2
좀 더 복잡한 산술 타입
이제 곱셈과 나눗셈 타입을 구현할 수 있습니다.
대부분의 작업이 유틸리티에서 수행되기 때문에 이러한 작업은 매우 간단합니다.
// 곱셈
type Multiply<A extends number, B extends number> =
MultiAdd<A, 0, B>;
// 몫
type Divide<A extends number, B extends number> =
MultiSub<A, B, 0>;
// 나머지
type Modulo<A extends number, B extends number> =
// A가 B보다 작으면
LT<A, B> extends true ? A : Modulo<Subtract<A, B>, B>;
Modulo에는 유틸리티가 필요하지 않습니다.
재귀 호출을 통해 반복한 횟수가 아니라 반복된 뺄셈 후에 남은 것만 알면 됩니다.
let twentyEight: Multiply<7, 4>; // `28`
let one: Divide<7, 4>; // `1`
let three: Modulo<7, 4>; // `3`
입력이 자연수인지 확인
그런데, 자연수 이외의 값을 사용하여 모든 종류의 산술을 수행하려고 하면 컴파일 오류가 발생합니다.
그것도 최대 재귀 깊이 제한 오류입니다.
Type instantiation is excessively deep and possibly infinite.(2589)
이것은 우리가 해당 연산의 구현에 사용한 튜플의 길이가 불연속적이고 양수인 것에서 기인합니다.
let one: Add<3, -2>;
let onePointSeven: Subtract<3, 1.3>;
type BuildTuple<L extends number, T extends any[] = []> =
T extends { length: L } ? T : BuildTuple<L, [...T, any]>;
type Add<A extends number, B extends number> =
Length<[...BuildTuple<A>, ...BuildTuple<B>]>;
type Length<T extends any[]> =
T extends { length: infer L } ? L : never;
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...(infer U), ...BuildTuple<B>]
? Length<U>
: never;
-2는 length : L에서 L(0 이상만 가능)이 절대 될수 없기에 계속 -2를 전달하여 무한 재귀를 발생시킵니다.
1.3도 마찬가지입니다.
이 문제를 해결하는 방법은 TypeScript의 새로운 템플릿 리터럴 타입을 사용하는 것입니다.
우리의 목표는 입력 패턴이 음수 또는 십진수와 일치하는지 확인하는 것입니다.
따라서 이러한 각 상황을 확인하기 위해 타입을 생성해 보겠습니다.
type IsPositive<N extends number> =
`${N}` extends `-${number}` ? false : true;
type IsWhole<N extends number> =
`${N}` extends `${number}.${number}` ? false : true;
let negativeNumber: IsPositive<-13>; // `false`
let positiveNumber: IsPositive<5>; // `true`
let float: IsWhole<1.3>; // `false`
let round: IsWhole<13>; // `true`
type IsValid<N extends number> =
IsPositive<N> extends true
? (IsWhole<N> extends true ? true : false)
: false;
type AreValid<A extends number, B extends number> =
IsValid<A> extends true
? (IsValid<B> extends true ? true : false)
: false;
type SafeAdd<A extends number, B extends number> =
AreValid<A, B> extends true ? Add<A, B> : never;
type SafeSubtract<A extends number, B extends number> =
AreValid<A, B> extends true ? Subtract<A, B> : never;
type SafeMultiply<A extends number, B extends number> =
AreValid<A, B> extends true ? Multiply<A, B> : never;
type SafeDivide<A extends number, B extends number> =
AreValid<A, B> extends true ? Divide<A, B> : never;
type SafeModulo<A extends number, B extends number> =
AreValid<A, B> extends true ? Modulo<A, B> : never;
let sum: Add<3, 4>; // `7`
let badSum: Add<3, 4.5>; // `never`
let anotherBadSum: Add<3, -4>; // `never`
지금까지 구현한 대수 타입 시스템의 한계
마무리
더 흥미로운 주제들이 있습니다 : 소수 만들기, 상대 소수 만들기, 몫으로 divisor 찾기 등을 구현해 보세요
위 예제들을 타입스크립트 플레이그라운드에서 직접 경험해 보기
비슷하지만 더 진보한 구현들을 보고싶다면...
https://dev.to/phenomnominal/i-need-to-learn-about-typescript-template-literal-types-51po
'FrontEnd' 카테고리의 다른 글
리액트로 XState 시작하기 (0) | 2022.04.28 |
---|---|
타입스크립트의 타입 시스템으로 틱택토(Tic Tac Toe) 구현하기 (0) | 2022.04.27 |
React Remix Framework로 알아보는 중첩 경로(nested routing) (0) | 2022.04.26 |
타입스크립트 함수의 시그니처와 리스코프 치환 원칙 (0) | 2022.04.09 |
타입스크립트의 타입 대수(type argebra)를 통해 타입 오류 분석하기 (0) | 2022.03.27 |