잠시 당신이 나무꾼이라고 가정해 보자
당신은 숲에서 가장 좋은 도끼를 가지고 있고 가장 일 잘하는 나무꾼이다
그런데 어느 날 산신령이 나타나서 나무를 자르는 새로운 언어
인 전기톱
을 설명서와 함께 주고 떠난다
당신은 사용하는 방법도 모르면서 설명서는 읽지 않고 늘 해온 방식대로 전기톱으로 나무를 두들겨댄다
당신은 새 언어를 이상하게 쓰면서 그것에 익숙해진다
그때, 누군가 나타나서 전기톱의 시동 거는 법을 알려준다
나는 당신 얘기를 하고 있다
당신은 설명서를 읽지 않은 나무꾼이고, 전기톱은 타입스크립트이다
당신은 타입스크립트를 잘 안다고 착각하고 있다
잘 안다고 생각한다면 혹시 이런 경험이 없는지 다시 생각해보길 바란다
pipe()
, curry()
함수를 타입할 수 있는가?d.ts
를 보고 도망친다T extends infer R ? [R] : never
의 의미를 아는가?이 글은 타입스크립트라는 전기톱의 시동을 거는 법을 알려주는 짧은 설명서이다
타입
을 더 깊게 이해
하고 쓰고
, 읽을
수 있기를 바라는 마음에서 글을 쓴다
타입스크립트의 타입 시스템은 튜링 완전함이 증명되었다
👨🏻🏫
튜링완전성(turing completeness)
어떤 프로그래밍 언어나 추상 기계가 튜링 기계와 동일한 계산 능력을 가진다는 의미이다. 이것은 튜링 기계로 풀 수 있는 문제, 즉 계산적인 문제를 그 프로그래밍 언어나 추상 기계로 풀 수 있다는 의미이다
제한 없는 크기의 기억 장치를 갖는 기계를 만드는 것이 불가능하므로, 진정한 의미의 튜링 완전 기계는 아마도 물리적으로 불가능할 것이다. 그러나, 제한 없이 기억 장치의 크기를 늘려갈 수 있다고 가정할 수 있는 물리적인 기계 혹은 프로그래밍 언어에 대해서는, 느슨하게 튜링 완전하다고 간주한다. 이런 맥락에서, 요즘 나온 컴퓨터들은 튜링 완전하다고 여겨지고 있다.
타입시스템의 튜링완전성 증명 👈 링크
타입 시스템이 튜링 완전하다는것은 우리가 타입레벨에서 프로그래밍
을 하고, 문제를 해결할 수 있다는것을 의미한다
예를 들면 타입시스템으로 소코반 게임을 만들 수도 있다!
런타임에 실행되는 코드가 아닌, 타입 시간에 평가(evaluate)되는 코드이다
👨🏻🏫
타입 시간
IDE에서 타이핑하는 시간을 의미한다, IDE를 통해서 일종의 REPL을 수행한다
P는 IDE의 커서위에 나타나는 콤보박스이다
이제 타입 시스템에 대해서 깊게 알아보며 어떻게 이것이 가능한지 살펴볼 것이다
타입 스크립트의 타입 시스템은 작은 순수 함수형 언어
다
이에 대해서 알아보기 이전에 몇가지 짚고 넘어가려한다
👨🏻🏫
공리(axiom)
어떤 한 형식체계에 관한 논의를 위한 전제로 주어진 공리들의 집합을 공리계(公理系)라고 부른다.
공리와 정리가 섞여있긴 하지만 이 챕터의 내용은 따지지 말고 당연한것으로 받아들이자는 의미에서 공리계라고 이름했다
number
, string
같은 무한 집합이 있다A | B
은 A와 B의 합집합A & B
은 A와 B의 교집합전체집합
공집합
A | never
는 A
A & never
는 never
any
는 모든 타입의 부분집합이자 상위집합이다 🤷A | any
는 any
A & any
는 any
20, 15, 30, ...
true, false
"hello", "world", ...
10000n, ...
{ key: "value" }
[1, 2, 3]
number[]
타입시스템의 함수는 제네릭이다
/** 런타임에서 실행되는 아래 함수의 타입레벨 프로그래밍 버전
* const f = (a: A, b: B): A | B => a | b
*/
type F<A, B> = A | B
type Push<X, XS extends X[]> = [...XS, X]
X[]
X[]
의 부분집합으로 제한된다X[]
의 부분집합이다 = XS가 X[]
에 대입 가능하다리스코프 치환원칙
👨🏻🏫
리스코프 치환원칙
자료형의 의미론적 처리를 보장하기 위한 법칙이다
A extends B
일 때, B를 A로 대체해도 동작에 문제가 없어야 한다
타입 시스템에서 분기는 conditional types에 의해 구현된다
type IF<A extends boolean, B, C> =
A extends true ? B : C
type val1 = IF<true, number, boolean> 👈 number
type val2 = IF<false, number, boolean> 👈 boolean
if-else
를 사용할 수 없다if-else
를 쓰고 ts 코드로 트랜스파일 할 수 있다extends
가 두번 쓰였는데 두개가 약간 다르다A extends boolean
A extends true ? B : C
디지털회로, 컴퓨터 구조
등의 과목에서 배운 논리 회로
를 전부 구현할 수 있다
👨🏻🏫
논리회로(logic circuit)
불 대수(Boolean algebra)를 이용하여 1개 이상의 논리 입력을 일정한 논리 연산에 의해 1개의 논리 출력을 얻는 회로이다
전자 회로(electronic circuit)의 구성 요소들을 이용하여 만든 논리 게이트(NAND, NOR, NOT 등)를 이용해 원하는 동작을 구현하는, 현대의 디지털 시대를 이끈 장본인 격의 학문이다
가독성을 위해 카르노맵을 사용합니다
AND | 0 | 1 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
type AND<A extends boolean, B extends boolean> =
[A, B] extends [true, true] ? true : false
Not | 0 | 1 |
---|---|---|
always | 1 | 0 |
type NOT<A extends boolean> =
A extends true ? false : true
NAND | 0 | 1 |
---|---|---|
0 | 1 | 1 |
1 | 1 | 0 |
type NAND<A extends boolean, B extends boolean> =
NOT<
AND<A, B>
>
NOT
, AND
함수를 재사용한다모든 논리 게이트는 NAND 만으로 구성 가능
하다NAND
를 재사용해서 구현 가능하다infer
infer
키워드는 Generic을 위한 Generic이다
인자로 리스트를 받아 길이를 반환하는 함수 LengthOf<T>
를 어떻게 구현할 지 생각해보자
indexed-access-types를 이용해서 T['length']
를 접근하면 될 것 같다
type LengthOf<T> = T['length']
[] constraint
를 쓴다.length constraint
를 사용한다type LengthOf<T extends unknown[]> = T['length']
type LengthOf<T extends { length: r }> = r
처럼 쓸 수 있다면 좋겠지만 아쉽게도 위의 r
표현은 불가능하다
대신 타입스크립트는 infer
키워드를 제공해준다
type L1<T extends { length: infer R}> = R ❌
type L2<T extends { length: unknown}> =
T extends { length: infer R } ? R : never ✅
extends
의 분기검사를 할 때만 사용가능extends
의 우측에서만 쓰일 수 있다T
가 { length: infer R }
의 부분집합이고 R을 추론가능하면 R로 분기type F<T> = [
HeavyComputation<T>,
HeavyComputation<T>,
HeavyComputation<T>,
]
type F<T> =
HeavyComputation<T> extends infer R ? (
[R, R, R]
) : never 👈 절대 분기되지 않는 상황
infer R
을 사용infer
키워드가 분기 검사를 위한 extends
의 우측에서만 쓰일 수 있기 때문에 불가피하게 사용type L<T extends Record<string, string>> = {
[k in keyof T]: `love ${T[k]}`
}
type 결과 = L<{ I: 'you'; you: 'me' }>
// 결과 = { I: 'love you', you: 'love me' }
in
키워드는 Union 타입을 순회한다type Includes<X, XS> =
XS extends [infer First, ...infer Rest] ? (
First extends X ? true : Includes<X, Rest>
) : false
type T = Includes<1, [1, 2, 3]> 👈 true
type F = Includes<4, [1, 2, 3]> 👈 false
const includes = (tar: number, arr: number[]): boolean => {
if (arr.length === 0) return false
const [head, ...rest] = arr
if (head === tar) return true
return includes(tar, rest)
}
타입 레벨에서 재귀(Recursion) 호출이 가능하다는것은 엄청나게 다양한 것을 가능하게 한다
예를들면, 순열을 만드는 함수 Permutation<T>
를 만들 수 있다
type Permutation<T, U = T> = [T] extends [never] ? [] : (
T extends U ? (
[T, ...Permutation<Exclude<U, T>>]
) : []
)
type 결과 = Permutation<'A' | 'B' | 'C'>
// 결과 = ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C']
// | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
Union 타입은 conditional 타입으로 분기검사를 할때 분배법칙으로 나눠지는 성질이 있다
type ToArray<T> =
T extends any ? 👈 항상 참, any는 모든 집합의 상위집합이자 부분집합
T[]
: never
type 결과 = ToArray<string | number>;
// 결과 = string[] | number[]
이는 Union타입이 분배법칙에 의해서 아래처럼 분배되었기 때문이다
ToArray<string> | ToArray<number>
// 다음과 같이 환원될 것이다
// string[] | number[]
만약에 (string | number)[]
를 얻기를 원한다면 아래처럼 써야한다
type ToArray<T> = [T] extends [any] ? Type[] : never;
위에서 언급했던 타입 시스템이 할 수 있는것들
을 다시한번 가져오겠다
동일성 체크를 제외한 모든것을 다뤘다
동일성 체크는 약간의 과제로 남기며 끝내려고 한다
동일성을 체크하는 Equal<T, Q>
함수를 만들어봐라
단, Equal함수는 인자로 any타입을 받을 수 있고, any타입에 무너지면 안된다
type Equal<T, Q> = 여기_코드_작성
type V1 = Equal<1, any> // should be false
type V2 = Equal<1, 1> // should be true
type V3 = Equal<1, 0> // should be false
type V4 = Equal<any, any> // should be true
type V5 = Equal<{ a: 10 }, { a: 20 }> // false
type V6 = Equal<{ a: 10 }, { a: 10 }> // true
type V7 = Equal<[1, 2, 3], [1, 2, 3]> // true
type V8 = Equal<[1, 2, 3], [any, 2, 3]> // false
type V9 = Equal<[], []> // true
type V10 = Equal<[1, 2], [1, 2, 3]> // false
type V11 = Equal<1 | 2 | 3, 2 | 3 | 1> // true
type V12 = Equal<1 | 2, 1 | 3 | 2> // false
type ExpectTrue<T extends true> = T
type ExpectFalse<T extends false> = T
type tests = [
ExpectFalse<V1>,
ExpectTrue<V2>,
ExpectFalse<V3>,
ExpectTrue<V4>,
ExpectFalse<V5>,
ExpectTrue<V6>,
ExpectTrue<V7>,
ExpectFalse<V8>,
ExpectTrue<V9>,
ExpectFalse<V10>,
ExpectTrue<V11>,
ExpectFalse<V12>,
]
힌트를 조금 주자면 아래 코드를 생각해보는것 부터 시작해라
type Equal<T, Q> =
T extends Q ? (
Q extends T ? true : false
) : false
동일성 체크를 구현한다면 아래처럼 테스트 코드를 먼저 작성하고 타입을 작성하는 TDD를 할 수 있다
type KebabCase<T extends string, P extends string> =
아직_작성하지_않은_함수
type Expect<T extends true> = T
type cases = [
Expect<Equal<KebabCase<'FooBarBaz'>, 'foo-bar-baz'>>,
Expect<Equal<KebabCase<'fooBarBaz'>, 'foo-bar-baz'>>,
Expect<Equal<KebabCase<'foo-bar'>, 'foo-bar'>>,
Expect<Equal<KebabCase<'foo_bar'>, 'foo_bar'>>,
Expect<Equal<KebabCase<'Foo-Bar'>, 'foo--bar'>>,
Expect<Equal<KebabCase<'ABC'>, 'a-b-c'>>,
Expect<Equal<KebabCase<'-'>, '-'>>,
Expect<Equal<KebabCase<''>, ''>>,
Expect<Equal<KebabCase<'😎'>, '😎'>>
]
type Equal<T, Q> = IsAny<T> extends true ? IsAny<Q> extends true ? true : false : T extends Q ? (
Q extends T ? (
XOR<IsAny<T>, IsAny<Q>> extends true ? false : (
T extends [infer First, ...infer Rest] ?
Q extends [infer QFirst, ...infer QRest] ?
Equal<First, QFirst> extends true
? Equal<Rest, QRest> : false : false : true
)
): false
) : false;
구현이 중복된거같긴한데.... 몸비틀어서 작성해봤습니다
잘보고갑니당