kakasoo

Math Types(1) - 타입으로 사칙연산하기 본문

프로그래밍/TypeScript

Math Types(1) - 타입으로 사칙연산하기

카카수(kakasoo) 2023. 4. 2. 17:50
반응형

타입스크립트 언어는 자바스크립트와 타입 레벨을 합친 언어다.

따라서 타입을 지정하는 것 뿐만 아니라 직접 타입을 구현할 수 있는 수준이 되어야 타입스크립트 쓰는 것이다.

하지만 타입 레벨에서는 언어에서 지원하는 사칙연산이 존재하지 않기 때문에 여기서부터 난이도가 급상승한다.

따라서 수학 공식에서 사용할 수 있는 타입들을 미리 구현하고 달달 외우면 반대로 난이도를 하락시킬 수 있다.

아래는 모두 내가 사용하고자 만든, 수학 법칙처럼 사용할 수 있는 유틸리티성 타입들이다.

 

Length

type Length<T extends any[]> = T['length'];

type answer0 = Length<[]>; // 0
type answer1 = Length<[1]>; // 1
type answer2 = Length<[1,2]>; // 2
type answer3 = Length<[1,2,3]>; // 3

타입 레벨에서는 사칙연산이 없기 때문에, 이를 대신하기 위해서 튜플의 Length를 이용해서 계산을 한다.

Length 타입은 튜플의 length를 출력하기에 마치 값 레벨에서의 return 키워드 같은 역할을 수행하게 된다.

 

ArrayToUnion

type ArrayToUnion<T extends any[]> = T[number];

type answer0 = ArrayToUnion<[1,2,3]>; // 1 | 2 | 3

어떤 배열 ( 이후부터는 배열 대신 튜플이라는 용어를 사용할 것인데, 이는 튜플이 고정된 길이의 배열이기 때문 ),

즉 튜플의 값들을 의미하기 때문에 Array를 Union으로 변환한 것과 동일한 역할을 한다.

튜플을 이용해 연산을 진행할 때, 최종적인 답은 튜플 혹은 유니온 둘 중 하나로 반환되어야 하기 때문에,

Length와 마찬가지로 return 키워드를 대신하기 위해 만들었다.

 

NToNumber, NToNumerTuple

type NToNumber<N> = N extends number ? N : never;
type NToNumberTuple<N> = N extends number[] ? N : never;

타입스크립트 컴파일러에게 타입이 number 또는 number[]임을 명시해주기 위해서 사용한다.

일반적인 경우에는 사용할 일이 없지만 infer와 같이 가상의 타입을 사용하는 경우에는 직접 명시가 필요하다.

이는 infer 키워드가 분기문을 통해 나오기 때문이다.

예컨대 제너릭 타입 T가 number[] 인 경우에도 내부 요소를 infer F로 추론한 경우 F가 number인 것은 아니다.

추가 해설

type FirstElementAdded10 = [1,2,3] extends [infer F, ...infer Rest] ? 
	Add<F, 10> : never;
	// error: This type parameter might need an `extends number` constraint.
type FirstElementAdded10 = [1,2,3] extends [infer F, ...infer Rest] ? 
	Add<NToNumber<F>, 10> : never;

F는 누가 봐도 number 타입이지만 infer 태그를 통해서는 number인지 아닌지 추론이 안된다.

이는 extends 키워드를 거쳤기 때문으로, 한 번 더 number임을 명시해줄 필요가 생긴다.

 

Push

type Push<T extends any[], V> = [...T, V];

type answer0 = Push<[], any>; // [any]
type answer1 = Push<[1,2,3], number> // [1, 2, 3, number]

타입 Push는 튜플에 새로운 타입 V를 삽입하여 튜플을 확장하는 타입이다.

기존 튜플을 T라고 할 때 Push의 결과는 T에 새로운 타입 V가 추가된 […T, V] 형태가 된다.

Push 타입은 바로 다음 설명할 NTuple<T>를 만드는 데에 필수적인 타입이다.

 

Pop

type Pop<T extends any[]> = T extends [...infer F, infer R] ? R : never;

type answer0 = Pop<[1,2,3]>; // 3

pop은 배열의 마지막 요소를 내보내는 타입을 의미한다.

단, 배열의 pop 메서드와 달리 원본 타입인 T를 변형하는 부가적인 효과는 없다.

 

PopAfter

type PopAfter<T extends any[]> = T extends [...infer F, infer R] ? F : never;

type answer0 = PopAfter<[1,2,3]>; // [1, 2]

Pop 타입의 문제를 해결하기 위해 Pop으로 제거된 요소를 제외한, 나머지 튜플을 추론하는 타입을 만든다.

Pop과 동일한 구현이지만 내보내는 대상이 infer R이 아닌 F로 된다.

 

NTuple

type NTuple<N extends number, T extends any[] = []> = 
	Length<T> extends N ? 
		T : 
		NTuple<N, Push<T, any>>;

type answer0 = NTuple<0>; // []
type answer1 = NTuple<1>; // [any]
type answer2 = NTuple<2>; // [any, any]
type answer3 = NTuple<3, [1]>; // [1, any, any]

NTuple은 N 만큼의 크기를 가지는 튜플을 의미하는 타입이다.

튜플 T의 길이가 N일 때는 T를 반환하고,

그렇지 않을 때는 기존 튜플에 any 타입 하나를 Push한 NTuple을 재귀적으로 반환한다.

즉, N에 도달할 때까지 기존 튜플에 any 타입을 계속 확장한 결과를 반환하기 때문에 NTuple이다.

 

Add

type Add <N1 extends number, N2 extends number> = 
	Length<[...NTuple<N1>, ...NTuple<N2>]>;

type answer0 = Add<10, 32>; // 42

Add 타입은 두 수 N1, N2를 받아서 더해주는 타입을 의미한다.

NTuple을 이용해서 각각 N1, N2 크기의 튜플을 만들어준 다음에 그 둘을 전개하여 하나의 튜플을 만든다.

그리고 그 하나의 튜플에 대해서 Length 타입을 이용해서 전체 길이를 구하면 그게 두 수의 합이 된다.

 

Sub

type Sub<A extends number, B extends number> = 
	NTuple<A> extends [...infer U, ...NTuple<B>] ? Length<U> : never;

type answer0 = Sub<3, 1>; // 2

타입 Sub는 두 수의 차이 ( A - B ) 를 구하는 타입이다.

두 수의 차이를 구하기 위해서 NTuple<A>가 NTuple<B>만큼을 제외할 때 나오는 나머지 튜플 크기를 구한다.

 

LessThan

type LessThan<N extends number, T extends any[] = []> = 
	Length<T> extends N ? T : LessThan<N, Push<T, Length<T>>>;

type answer0 = LessThan<10>; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

LessThan의 구현 방식은 NTuple과 동일하다.

대신 Push에 아무 타입 any를 넣는 대신 현재 배열의 크기를 넣는 식으로 진행된다.

현재 배열의 크기는 재귀를 타고 들어갈 때마다 점점 커지기 때문에 ( N에 도달할 때 까지 ),

결과적으로 0부터 N - 1 까지의 정수가 된다.

 

LessThanEqual

type LessThanEqual<N extends number, T extends any[] = []> = 
	LessThan<NToNumber<Add<N, 1>>>;

LessThan이 0부터 N - 1까지기 때문에 LessThanEqual은 N보다 1 큰 숫자를 대입해야 한다.

따라서 Add 타입을 이용해 1 더 큰 수를 넣은 LessThan을 반환해주면 된다.

 

Sum

type Sum <T extends number[]> = Length<T> extends 0 ?
	0 : 
	Add<
		NToNumber<Pop<T>>, // T를 pop한 값과,
		NToNumber< 
			Sum<
				NToNumberTuple<PopAfter<T>> // T를 pop하고 남은 나머지 튜플을 재귀적으로 sum한 값
			>
		>
	>

type answer0 = Sum<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]> // 55

타입 Sum은 number로만 이루어진 튜플의 모든 요소 값을 합산하는 타입을 의미한다.

이 타입은 Add를 이용한 재귀로 구성되며,

초기 튜플 T에서 pop하여 꺼낸 하나의 숫자와, 나머지 숫자들을 재귀적으로 더한 튜플의 Sum을 합한다.

앞서 정의한 유틸리티성 타입들이 있기 때문에 이 타입을 손쉽게 구현할 수 있게 되었다.

 

GaussSum

type GaussSum<T extends number> = Sum<LessThanEqual<T>>;

type answer0 = GaussSum<30>; // 465

가우스는 어릴 때 1부터 100까지의 숫자를 더하라는 선생님의 말을 듣고 바로 답을 말했다.

가우스는 등차수열의 합을 알고 구할 줄 알았다.

GaussSum은 1부터 T까지의 수를 더하는 타입이다.

단, 이 타입은 타입스크립트 재귀 한계로 인해 GaussSum<30> 까지밖에 동작하지 않는다.

 

NNTuple

type NNTuple<N1 extends number, N2 extends number, P extends any[] = []> =
  Sub<N1, 1> extends never ?
		[] :
		[
			...NNTuple<
				Sub<N1, 1>,           // N1-1
				N2,                   // N2
				[...P, ...NTuple<N2>] // 지금까지 종합한 P와 NTuple<N2>를 합한 새 NNTuple, 사실 빈 튜플이어도 동작한다.
			>, 
			...NTuple<N2>           // N1이 1인 경우 재귀를 타지 않기 때문에 1번만 반환하면 된다.
		];

type answer0 = NNTuple<1,3>; // [any, any, any]
type answer1 = NNTuple<2,3>; // [any, any, any, any, any, any]

NNTuple은 N1과 N2를 곱한 크기만큼을 가지는 NTuple과 동일하다.

즉, N1N2Tuple은 N2 튜플을 N1 만큼 만드는 것과 같으니,

NNTuple은 그 수 N1이 0이 될 때까지 1을 제하면서 NTuple을 만들고 하나의 튜플로 합치는 일을 하는 셈이다.

따라서 N1에 1을 뺀 수가 0 또는 양의 정수일 때는 빈 배열을,

N1이 아직 1 이상의 값을 가지고 있을 때에는 NNTuple에 N1 - 1과 N2, 그리고 지금까지 합한 배열을 전달한다.

 

Multiply

type Multiply<T1 extends number, T2 extends number> = Length<NNTuple<T1,T2>>;

type answer = Multiply<11, 909>; // 9999

NNTuple을 이용해서 두 수의 곱 만큼을 Length로 가지는 튜플을 만들고 그 튜플의 Length를 뽑는다.

즉, 그 두 수의 곱을 의미하는 Multiply 타입이 완성된다.

타입스크립트 재귀 한계가 정확히 몇인지는 모르겠지만 곱셈을 이용하는 것은 숫자 10,000 미만인 듯 하다.

 

추후 구현할 타입

나누기 타입

나눗셈은 단순히 사칙연산을 위해 필요할 뿐만 아니라 튜플을 일정한 수로 분리하기 위해 필요하다.

이 타입이 있으면 큰 수의 계산을 할 때 작은 수의 계산으로 여러 번 분리한 후 진행하면 되기 때문에,

지금까지 구현된 타입들의 문제점들을 대거 해결할 수 있다.

나머지 타입

나누기 타입은 소수점을 표현할 수 없다.

튜플에서는 소수점을 Length로 가지지 않기 때문이다.

따라서 나머지 타입을 구현해야만 완벽한 사칙연산이 가능해진다.

제곱 타입

type Square<N1, N2> = any; // N1^N2를 의미하는 숫자

최종적으로 구현하고자 하는 타입, Square은 N1의 N2 제곱을 의미하는 타입이다.

매우 큰 수가 나오기 때문에 앞서 나누기 타입을 구현하여 타입을 작은 수부터 나눠 진행할 예정이다.

아직은 망상이라 이게 가능한 타입인지는 모르겠다.

혹시라도 이 타입을 구현해낸 적 있는 사람이 있다면 제발 공유해줬으면.

 

 

 

Math Types(2) - 타입으로 나누기, 나머지 연산 구현하기

Math Types(1) - 타입으로 사칙연산하기 타입스크립트 언어는 자바스크립트와 타입 레벨을 합친 언어다. 따라서 타입을 지정하는 것 뿐만 아니라 직접 타입을 구현할 수 있는 수준이 되어야 타입스

kscodebase.tistory.com

 

반응형

'프로그래밍 > TypeScript' 카테고리의 다른 글

KebabCase type  (0) 2023.04.02
Merge, 두 객체 합병하기  (0) 2023.04.02
StringToUnion  (0) 2023.04.01
Permutation, 타입으로 순열 구현하기  (0) 2023.03.31
Length Of String, 문자열의 길이를 출력하는 타입  (0) 2023.03.27