본문 바로가기

FrontEnd

타입스크립트의 타입 대수(type argebra)를 통해 타입 오류 분석하기

반응형
타입스크립트
Type algebra는 TypeScript에서 많이 알려지지 않은 주제이며 TypeScript의 몇 가지 단점을 이해하는 데 필수적인 주제입니다.

Algebras (대수)

집합 대수가 |,&에서 모두 분배법칙이 성립하며 타입스크립트의 타입 합집합 교집합은 집합 대수를 따름을 이해하면 됩니다.

우리 모두 수학 수업에서 몇 가지 대수 법칙을 배웠습니다.
  • 곱셈은 덧셈에 대해 왼쪽 분배 법칙 성립: a * (b + c) === (a * b) + (a * c)
  • 곱셈은 덧셈에 대해 오른쪽 분배 법칙 성립: (a + b) * c === (a * c) + (c * c)
  • 덧셈은 곱셍에 대해 분배법칙이 성립하지 않습니다.
  1.  
그리고 우리가 방금 본 일반 대수학(ordinary algebra)과 약간 다른 부울 대수학(boolean algebra)이 있습니다.
  • &&은 ||에 대해 왼쪽 분배 법칙 성립 :  a && (b || c) === (a && b) || (a && c)
  • &&은 ||에 대해 오른쪽 분배 법칙 성립: (a || b) && c === (a && c) || (b && c)
  • ||은 &&에 대해 분배 법칙이 성립하지 않습니다.
마지막으로 집합 대수학(Set Algebra)이 있습니다. Set Theory에는 합집합(∪, TypeScript의 | 연산자) 및 교집합(∩, TypeScript의 & 연산자) 연산이 있습니다.
  1. 교집합 연산(&)은 합집합 연산(|)에 대해 좌, 우 분배 법칙이 성립합니다. 즉, A&(B|C) ===  (A&B) | (A&C) ===  (B&A) | (C&A) 입니다. 
  2. 합집합 연산(|)은 교집합 연산(&)에 대해 좌, 우분배 법칙이 성립합니다.  즉,  A | (B & C)  === (A | B) & (A | C) === (B|A) & (C|A) 입니다.
주 : 실제로는 논리적 & (conjunction), 논리적 |(disjunction) 입니다.
참고로 곱셈과 덧셈의 관계(부울 대수 포함)을 Ring 대수 구조라 합니다.
집합 대수의 대수구조는 Semiring 대수구조라 합니다.
TypeScript는 Set Theory과 매우 관련이 있으며 타입스크립트의 타입에 대한 합집합 및 교집합 연산은 집합 이론의 대수 법칙을 따릅니다.
TypeScript의 맥락에서 저는 이를 타입 대수라고 부릅니다.

A & (B | C)와 같은 복잡한 타입을 매일 작성할지 의심스럽긴 하지만

때로는 TypeScript 오류 메시지를 해독하기 위해 타입 대수를 사용해야 합니다

Apply type algebra (타입 대수 사용하기)

이제 구체적인 (인위적인) 예제를 살펴보고 혼란스러운 타입 오류를 이해하기 위해 타입 대수학을 적용하는 방법을 살펴보겠습니다.
Conference와 Meetup이라는 두 가지 타입의 기술 이벤트가 있다고 상상해 보십시오.
Conference는 직접 대면 또는 Zoom을 통해 가상으로 온라인으로 개최할 수 있으며, Meetup은 물리적인 장소에서 직접 개최해야 합니다.
이를 모델링하기 위해 두 가지 타입의 이벤트가 결합된 TechEvent 타입이 있습니다.
마지막으로 이벤트가 온라인으로 개최됨을 의미하는 {isVirtual: true}만 지정하는 IsVirtual 개체 타입이 있습니다.
type Conference = {type: 'conference', isVirtual: boolean} 
type Meetup = {type: 'meetup', isVirtual: false} 
type TechEvent = Conference | Meetup
type IsVirtual = {isVirtual: true} 

// We intersect IsVirtual with conference and meetup, then explore the resulting type.
type VirtualEvent = IsVirtual & TechEvent
VirtualEvent 타입을 사용하여 회의에 대한 변수를 입력합니다.
const conference: VirtualEvent = {type: 'conference', isVirtual: true} // ✅
isVirtual 속성을 엉망으로 만들면 isVirtual이 true여야 하는 유형 오류가 발생합니다.
const conference: VirtualEvent = {type: 'conference', isVirtual: false} // ❌ type 'false' is not assignable to type 'true'
IsVirtual & TechEvent 타입이 시작점입니다.
만약 우리가 union 위에 intersection을 분배한다면 이 타입에 대해 생각하기가 더 쉽습니다.
// By applying type algebra, we get three equivalent types:
type VirtualEvent = IsVirtual & TechEvent
type VirtualEvent = IsVirtual & (Conference | Meetup)
type VirtualEvent = (IsVirtual & Conference) | (IsVirtual & Meetup)
Conference 변수가 isVirtual이 true를 필수로 요구하는 이유를 이해하기 어렵지 않습니다.
Conference 타입의 조건이 isVirtual: boolean이고 IsVirtual 타입이 isVirtual: true기 때문에
두 타입의 교집합은 isVirtual: boolean & true이 됩니다.
boolean & true는 true와 동일합니다.
이것이 위의 타입 오류가 isVirtual 속성에 대해 true를 요구하는 이유입니다.

여기까지는 꽤 간단해 보입니다. 그러나 Meetup 타입의 경우 상황이 훨씬 더 복잡합니다.

Meetup에는 isVirtual: false가 있고 IsVirtual에는

isVirtual: true가 있습니다.

VirtualEvent 타입에서 이들을 교차하면 예상치 못한 일이 발생합니다.

const meetup: VirtualEvent = {type: 'meetup', isVirtual: true} // ❌ Type '"meetup"' is not assignable to type '"conference"'
위의 코드는 타입 오류로 인해 컴파일되지 않습니다.
이는 놀라운 일이 아닙니다. 하지만 타입 오류 자체는 흥미롭습니다.
Type '"meetup"' is not assignable to type '"conference"'라고 표시됩니다.
변수는 conference가 아니라 meetup을 위한 것입니다.
여기서 컴파일러는 무엇이 잘못되었는지 정확히 알려주지 않으므로 타입 대수학을 통해 스스로 문제를 해결해야 합니다.
주 : 이해가 안되면, 아래 보충설명 문단을 읽고 올라오세요.
 
  • 타입 VirtualEvent는 intersection에 의해 생성됩니다 - (IsVirtual & Conference) | (IsVirtual & Meetup)
  • 합집합의 오른쪽 IsVirtual & Meetup은 {isVirtual: true} & {type: 'meetup', isVirtual: false}이며, isVirtual 속성에 대한 true 와 false의 교집합이 공집합이기 때문에 전체가 never로 축소됩니다.
  • intersection 결과 (IsVirtual & Conference) | never 입니다만, Typescript는 union type에서 never를 자동으로 버립니다.

  • 최종 결과는 {type: 'conference', isVirtual: true} === isVirtual & Conference가 됩니다.
never 타입에 익숙하지 않은 경우 아래 게시물을 참조하세요

a blog post for never

 

다시 잘못된 할당으로 돌아가 봅시다.
const meetup: VirtualEvent = {type: 'meetup', isVirtual: true} // ❌ Type '"meetup"' is not assignable to type '"conference"'   
VirtualEvent 타입을 대수학 타입({type: 'conference', isVirtual: true})을 통해 얻은 동등한 버전으로 바꾸면 동일한 타입 오류가 발생합니다.
이제 컴파일러가 'meetup'을 'conference'에 할당할 수 없다고 보고한 이유가 명확해졌기를 바랍니다.
컴파일러는 union에 intersection를 분산하여 얻은 never 타입 때문에 union의 오른쪽 전체를 삭제했습니다.
{isVirtual: true} & {type: 'meetup', isVirtual: false}가 {type: 'meetup', isVirtual: never}여야 한다 생각할 수 있습니다.
실제 TypeScript 3.9 이전에는 그랬습니다.
이후 버전은 empty intersection이 존재하는 union case를 never로 바꿔버립니다.
(anyType | ({a:1} & {a:2} ) ) ==(anyType | never)
자세한 내용과 동기는 아래 PR을 확인하세요.

이 PR을 확인하세요!

보충설명 

객체 타입의 intersection 시 교집합 속성이 판별 속성(discriminant properties - 리터럴 타입 또는 리터럴 타입의 합집합)으로 간주되는지 여부에 따라 전체 타입을 never로 축소하는 경우가 있습니다.
 
아래 예에서는 string과 number가 판별 속성이 아니므로 name 속성만 never가 됩니다.
type Foo = {
    name: string,
    age: number
    }
type Bar = {
    name: number,
    age: number
}

type Baz = Foo & Bar // {name: never, age: number}
 
boolean은 판별 속성(true | false의 합집합) 취급이기 때문에 전체 타입 Baz가 never로 축소됩니다.
type Foo = {
    name: boolean,
    age: number
    }

type Bar = {
    name: number,
    age: number
}

type Baz = Foo & Bar // never

(주 : 둘 다 못써먹는 타입인건 동일합니다.)

마무리 : 타입 대수를 확장하지 마세요.

TypeScript에는 분배 법칙에 적합한 후보라고 생각할 수 있는 몇 가지 type annotation이 있지만 실제로는 그렇지 않습니다.
  • `(number | string)` [] vs `number[] | string[] `
    • 전자는 원소가 number 혹은 string인 타입인 배열.
    • 후자는 number배열이거나 string배열
  • `keyof (A & B)` vs `keyof A & keyof B`
    • 전자는 타입 A와 B의 intersection에 대한 속성명 리터럴 문자열의 합집합 (교집합 후 키 추출)
    • 후자는 타입 A 및 B의 속성명 리터럴 문자열의 교집합 (키 추출 후 키로 교집합)
  • `typeof foo & typeof bar` and `typeof (foo & bar)`
    • 후자는 유효하지 않은 타입스크립트 구문입니다.
    • 주 : 왜일까요? foo가 변수라면 변수 공간에 있기 때문에 & 연산자를 적용할 수 없습니다.

참고 :

타입스크립트의 타입 대수

 

Type Algebra

Type algebra is a much underwritten topic in TypeScript, a topic that I found essential to understand some quirks in TypeScript. And there is boolean algebra, which is a little different than the ordinary algebra we just saw: Lastly there is set algebra. I

www.zhenghao.io

 

반응형