집합론을 이용해 타입스크립트를 좀 더 잘 이해해 봅시다.
아래 글의 번역입니다.
https://blog.thoughtspile.tech/2023/01/23/typescript-sets/
Making sense of TypeScript using set theory
I've been working with TypeScript for a long long time. I think I'm not too bad at it. However, to my despair, some low-level behaviors still confuse me: Why does 0 | 1 extends 0 ? true : false evaluate to false?I'm very ashamed, but I sometimes confuse "s
blog.thoughtspile.tech
오랫동안 TypeScript로 작업해 왔습니다.
저는 타입스크립트를 꽤 잘 쓴다고 생각합니다만,
실망스럽게도 몇 가지 저수준 동작을 여전히 이해하지 못하고 있었습니다.
- 0 | 1 extends 0 ? true : false는 왜 false인가요?
- 부끄럽지만 가끔 "서브타입"과 "슈퍼타입"을 혼동하기도 합니다.
- 타입 축소(narrowing) 및 확장(widening)은 뭐고 이게 슈퍼/서브타입과 무슨 상관이 있죠?
- { name: string } 및 { age: number }를 모두 만족하는 객체를 원하는 경우 &를 쓰는게 맞나요? |를 쓰는게 맞나요?
- 기능면에선 합치는(|)게 맞는것 같은데
- 해당 객체가 두 인터페이스를 동시(&)에 만족시켰으면 좋겠어요
- any와 unknown은 어떤 차이가 있죠?
- 제가 배운 것은 "Avoid Any, Use Unknown"과 같은 부정확한 암기법 입니다. 이유가 뭔가요?
- never는 정확히 뭐죠? "절대 발생하지 않는 값"은 이해가 안됩니다.
- whatever | never === whatever , whatever & never === never. ??
- 도대체 왜 const x: {} = true는 유효한 걸까요?
![](https://blog.kakaocdn.net/dn/bmBoSf/btrXCHhUWJW/6Uk5Kxb1xMJtETXzAI0Oek/img.png)
집합론
집합은 정렬되지 않은 객체 컬렉션 입니다.
쉽게 설명하기 위해,
두 개의 사과(ivan과 bob이라고 해봅시다), 일명 객체와
사과를 넣을 수 있는 가방,즉 일명 집합이 있다고 가정해 보겠습니다.
총 4개의 사과 집합을 만들 수 있습니다.
- 사과 ivan이 든 가방, { ivan }
- 집합은 내부에 집합 아이템이 있는 중괄호로 표시됩니다.
- 마찬가지로 가방에 사과 밥 { bob }을 넣을 수 있습니다.
- 두 개의 사과가 든 가방, { ivan, bob }.
- 이것은 전체 집합(Universe)이라 불립니다. 왜냐하면 현재 우리 세상에는 이 두 개의 사과 외에는 아무것도 없기 때문입니다.
- 빈 가방, 일명 공집합(empty set), {}. 이것은 특별한 기호 ∅로 표현합니다.
집합은 종종 "벤 다이어그램(venn diagrams)"으로 그려지며
각 집합은 원으로 표시됩니다.
모든 항목을 나열하는 것 외에도 조건별로 집합을 만들 수도 있습니다.
ivan은 빨간색이고 bob은 녹색 사과라 한다면,
"R은 빨간 사과 집합"이 { ivan }을 의미한다고 할 수 있습니다.
- Union C = A ∪ B는 A 또는 B에 있는 모든 요소를 포함합니다.
- A ∪ ∅ = A 입니다.
- Intersection : C = A ∩ B는 A와 B에 모두 존재하는 모든 요소를 포함합니다.
- A ∩ ∅ = ∅ 를 주의하세요.
- Difference C = A \ B는 A에는 있지만 B에는 없는 모든 요소를 포함합니다.
- A \ ∅ = A 입니다.
이제 지금까지 배운 내용이 타입에 어떻게 매핑되는지 살펴보겠습니다.
집합론이 타입과 어떤 관계가 있나요?
중요한 점은 "타입"을 JavaScript 값 집합으로 생각할 수 있다는 것입니다.- 전체 집합은 JS 프로그램이 생성할 수 있는 모든 값입니다.
- 타입(타입스크립트 타입이 아니라 일반적인 타입)은 JS 값의 집합입니다.
- 일부 타입은 TS로 표시될 수 있지만 몇몇 타입은 아닙니다.
- "0이 아닌 숫자"와 같이 타입스크립트로 표현할 수 없는 타입이 있습니다.
- A extends B는 조건부 타입(conditional types)과 제네릭 제약조건(generic constraints)의 설명과 같이 "A는 B의 서브타입"으로 읽을 수 있습니다.
- 유니온(|)과 인터섹션(&) 타입 연산자는 두 집합의 합집합과 교집합일 뿐입니다.
- Exclude<A, B>는 A와 B가 모두 유니온 타입일 때만 작동한다는 점을 제외하면 TS의 difference 연산자라 생각하면 좋습니다.
- never는 공집합 입니다.
- 증명
- A & never = never
- A | never = 모든 타입 A에 대해 A
- Exclude<0, 0> = never.
- 증명
- 타입 A의 서브타입은 타입 A의 서브셋입니다. 슈퍼타입은 슈퍼셋 입니다.
- 타입 넓히기(widening)는 일부 추가 값을 허용하여 타입 집합을 더 넓게 만듭니다.
- 타입을 좁히면(Narrowing) 특정 값이 제거됩니다. 기하학적 의미를 갖습니다.
Boolean types
가장 간단한 타입부터 시작해 봅시다.
- 리터럴 타입은 각각 단일 값인 true 및 false입니다.
- boolean은 부울 값입니다.
- 공집합은 never 입니다.
![](https://blog.kakaocdn.net/dn/dpD47B/btrXDzX7XvF/v0s9d7KguThZPeBVuGzdVk/img.png)
- boolean = true | false 입니다.
- 실제로 타입스크립트가 이를 구현하는 방법입니다.
- true는 boolean의 서브셋/서브타입 입니다.
- never는 공집합이므로 true, false 및 boolean의 서브셋/서브타입 입니다.
- &는 교집합입니다
- false & true = never
- boolean & true = (true | false) & true = true (전체집합인 boolean은 교집합의 항등원)
- true & never = never
- Exclude는 집합 difference를 정확하게 계산합니다: Exclude<boolean, true> -> false
- 내부적으로 boolean이 정확하게 true|false로 구현되어 있기 때문입니다.
- |는 유니온 입니다.
- true | never = true, boolean | true = boolean
- (리터럴의 전체집합인 boolean은 다른 합집합 대상을 삼켜버립니다.)
- true | never = true, boolean | true = boolean
이제 약간 까다로운 extends를 다루어 보겠습니다.
type A = boolean extends never ? 1 : 0;
type B = true extends boolean ? 1 : 0;
type C = never extends false ? 1 : 0;
type D = never extends never ? 1 : 0;
- A는 0
- B는 1
- C는 1
- D는 1
- never extend null은 여전히 유효합니다.
- null & boolean은 JS 값이 동시에 2개의 서로 다른 JS 타입이 될 수 없기 때문에 never입니다.
![](https://blog.kakaocdn.net/dn/cc4OTP/btrXBbdfPWj/j0icw4LhKTM9BAAJwmvuK1/img.png)
Strings and other primitives
유한한 컴퓨터 메모리엔 유한한 문자열만 표현할 수 있기 때문에 거짓말일 수 있습니다만,
a) 모든 문자열을 열거하는 것은 실용적이지 않으며
b) 타입 시스템은 더러운 실제 제한에 대해 걱정하지 않고 순수한 추상화 위에서 동작할 수 있습니다.
- |(union)을 사용하면 유한한 String 집합을 구성할 수 있습니다.
- type Country = 'de'|'us';
- 무한한 값 목록을 작성할 수 없기 때문에 길이가 2보다 큰 모든 문자열과 같은 무한 집합은 표현할 수 없습니다.
- 멋진 탬플릿 리터럴 타입(template literal types)을 이용하면 몇몇 무한 집합을 만들 수 있습니다.
- type V = `v${string}`은 v로 시작하는 문자열 입니다.
![](https://blog.kakaocdn.net/dn/s4tk4/btrXDBhkwrs/97Aow4tRK6eg9KzyDjsKI0/img.png)
interface와 object 타입
const x: {} = 9가 왜 정상적일까요? 이를 이해하지 못하고 있다면, 이 문단은 여러분을 위한 것입니다.
TS 객체 타입/레코드/인터페이스에 대한 우리의 가정은 잘못된 멘탈 모델 위에 구축되어 있습니다.
null 및 undefined는 {}를 충족하지 않습니다. 속성을 읽으려고 하면 오류가 발생하기 때문입니다.
직관적이지는 않지만, 의미가 있습니다.
"| 또는 &" 문제로 돌아가면,
&와 |는 객체 모양을 대상으로 하는게 아니라, 값 집합을 대상으로 합니다.
따라서 name과 age를 모두 가진 객체 집합을 나타내려면
{ name: string } & { age: number }가 필요합니다. (and는 & 입니다.)
이제 모든 것을 타입 맵에 추가해 보겠습니다.
![](https://blog.kakaocdn.net/dn/LLHzK/btrXEB9faqG/ooR11WnPGDQJRfzaKkOkMK/img.png)
extends
- A is a sub-type of B
- A is a subset of B
- 0 | 1 extends 0은 false입니다.
- 2개 요소 집합 {0, 1}은 1개 요소 {0}의 하위 집합이 아니기 때문입니다.
- ({0,1}이 기하학적 의미에서 {1}을 확장하더라도).
- never extends T는 항상 참입니다.
- 이는 공집합인 never가 모든 집합의 부분 집합이기 때문입니다.
- T extends never는 T가 never인 경우에만 true입니다.
- 왜냐하면 공집합에는 자신을 제외한 서브셋이 없기 때문입니다.
- T extends string은 T가 스트링, 리터럴, 리터럴 유니온 또는 템플릿이 될 수 있도록 허용합니다.
- 이들 모두가 스트링의 서브셋이기 때문입니다.
- T extends string ? string extends T ? true : false는 T가 정확히 string 타입인지를 확인합니다.
Unknown and Any
Typescript에는 임의의 JS 값을 나타낼 수 있는 두 가지 타입(unknown 및 any)이 있습니다.
일반적인 것은 unknown으로, JS 값의 전체집합 입니다.
// It's a 1
type Y = string | number | boolean | object | bigint | symbol | null | undefined extends unknown ? 1 : 0;
// a shorter one, given the {} oddity
type Y2 = {} | null | undefined extends unknown ? 1 : 0;
// For other types, this is 0:
type N = unknown extends string ? 1 : 0;
그러나 수수께끼 같은 측면이 있습니다.
- unknown은 다른 모든 프리미티브 타입의 합집합이 아니므로 Exclude<unknown, string>을 사용할 수 없습니다.
- unknown extends string | number | boolean | object | bigint | symbol | null | undefined 는 false 입니다
- 즉 unknown이 더 큰 집합의 서브타입 이라는 것입니다. enum이 의심됩니다.
그래도 대체로 unknown을 "가능한 모든 JS 값의 집합"으로 생각하는 것은 안전합니다.
하지만 any는 정말 이상합니다.
- any extends string ? 1:0은 0|1 입니다.
- 이는 나도 몰라요를 의미합니다.
- any extends never ? 1:0은 0|1 입니다.
- 이는 any가 아마도 공집합일 수 있음을 의미합니다.
![](https://blog.kakaocdn.net/dn/LuEzt/btrXz0bXkdr/w59kHHN9r0eDVuG9SYM5hK/img.png)
오늘 우리는 TS 타입이 기본적으로 JS 값의 집합이라는 것을 배웠습니다.
다음은 type-world에서 set-world의 매핑 사전입니다.
- 우리의 우주(전체집합) = 모든 JS 값 = unknown
- never = 공집합
- Subtype = narrowed type = subset
- supertype = widened type = superset.
- A extends B는 "A is subset of B"로 읽을 수 있습니다. (A는 B의 부분집합)
- Union 및 Intersection 타입은 사실 집합의 Union 및 Intersection 연산입니다.
- Exclude는 Union 집합 대상으로 동작하는 집합의 difference와 유사한 연산입니다.
맨 처음 문단의 질문에 대한 대답을 하겠습니다.
- 0 | 1 extends 0은 {0,1}이 {0}의 하위 집합이 아니기 때문에 거짓입니다.
- &,|은 집합을 대상으로 동작합니다. 모양을 대상으로 동작하는 것이 아닙니다.
- A & B는 둘 다를 만족하는 집합입니다.
- unknown은 모든 JS 값의 집합입니다.
- any는 색즉시공, 공즉시색의 역설입니다.
- never와 &하면 never입니다. 공집합이기 때문입니다.
- union에는 영향을 미치지 않습니다.
- const x:{}=true;는 정상입니다.
- TS 인터페이스는 속성 제약 기능만 있기 때문입니다.
- Primitive 밸류는 autoboxing에 의해 객체이며, 객체 리터럴 {}의 인터페이스를 만족합니다.
- 만약 Primitive Value가 아닌 객체를 표현하고 싶다면, {} & object를 사용합니다.
- 내장 object 타입은 '프리미티브가 아닌 JS 객체'를 의미합니다
참고
[타입스크립트] Object vs object vs {}
해당 스택오버플로우 게시물을 보고 요약한 글입니다. https://stackoverflow.com/questions/49464634/difference-between-object-and-object-in-typescript Difference between 'object' ,{} and Object in TypeScript Trying to figure out the diffe
itchallenger.tistory.com
'FrontEnd' 카테고리의 다른 글
[번역] 빅테크 프론트엔드 기술 인터뷰 : Html편 (0) | 2023.02.01 |
---|---|
[번역] 빅테크 프론트엔드 기술 인터뷰 : CSS편 (0) | 2023.02.01 |
Javascript로 Python range 함수 구현하기 (0) | 2023.01.31 |
[번역]CSS 셀렉터 성능 최적화 (0) | 2023.01.31 |
[Vue3] 2023년, Vue3은 어떻게 달라질 것인가? (0) | 2023.01.29 |