원문: https://ivov.dev/notes/typescript-and-set-theory
프레젠테이션:
- 분산 조건부 유형 - TypeScript Berlin Meetup #8
집합론은 집합(즉, 요소의 집합)에 전념하는 수학의 한 분야입니다. 포함된 요소를 열거하여 집합을 정의할 수 있습니다.
또는 어떤 요소가 집합에 속하는지 결정하는 규칙을 기술합니다.
이 표기법은 " 가 인 집합 에 속하는 모든 요소 의 집합" 이라고 읽습니다. 이 함수는 predicate라고 불리며, 그 목적은 집합에 속하는 요소를 선택하는 것입니다. predicate를 filter()
함수에 대한 콜백으로 생각하세요. 입력의 항목이 우리가 만들고 있는 집합에 속하는 경우 true
를 리턴합니다. 는 집합의 구성원 자격을 갖추기 위해 모든 요소가 충족해야 하는 제약 조건입니다.
집합을 정의하면 주어진 요소와 집합의 관계를 설명할 수 있습니다. 요소는 세트의 멤버일 수도 있고 아닐 수도 있습니다 (즉, 다음과 같은 집합에 속할 수도 있습니다).
집합과 다른 집합의 관계를 설명할 수도 있습니다. 집합은 첫 번째 집합의 모든 요소가 두 번째 집합의 요소이기도 한 경우에만 다른 집합의 부분 집합(즉, 포함)입니다. 즉, 하나의 집합의 모든 항목이 다른 집합에 존재할 때 첫 번째는 두 번째의 부분 집합입니다.
또한 첫 번째 집합에는 존재하지 않는 요소가 두 번째 집합에도 하나 이상 있는 경우 첫 번째(작은) 집합은 첫 번째의 상위 집합인 두 번째(큰) 집합의 적절한 부분 집합입니다.
함축적으로, 적절한 부분집합이 아닌 다른 집합의 부분집합인 집합은 다른 집합과만 같을 수 있습니다. 집합은 전자의 모든 요소가 후자의 구성원이고 후자의 모든 요소가 전자의 구성원인 경우에만 다른 집합과 같습니다. 집합의 순서와 중복 무시 — 두 집합은 동일한 요소를 포함하는 경우에만 동일합니다.
그래픽으로:
반대로 하위 집합이 아닌 것을 고려합니다. 첫 번째 집합에 두 번째 집합의 구성원이 아닌 요소가 적어도 1개 존재하는 경우 집합은 다른 집합의 부분 집합이 아닙니다. 이것들은 교집합입니다.
그래픽으로:
마지막으로, 두 가지 특별한 경우입니다. Ø로 표시되는 공집합은 아무것도 포함하지 않는 집합입니다. 예를 들어, 서로소 집합 사이에 공통 요소를 포함하는 집합입니다. 정확히 말하면 공집합은 아무것도 아닌 것이 아니라 아무것도 담지 않는 그릇입니다.
그리고 그 반대는 U로 표시되는 합집합입니다. 모든 집합이 부분 집합이고 모든 요소가 구성원인 집합입니다. 수학에서 합집합은 논의의 경계를 나타냅니다.
추가 정보:
다음 섹션에 대한 간략한 소개입니다. 집합론에 대해 더 알아보려면:
- 이산 수학 및 응용(6장) - Susanna S. Epp
- 컴퓨터 과학을 위한 필수 이산 수학(5장) - Harry Lewis 및 Rachel Zax
- 추론 학습: 논리, 집합 및 관계 소개(3장) - Nancy Rodgers
집합론은 TypeScript의 유형에 대한 추론을 위한 정신적 모델을 제공합니다. 집합론의 관점을 통해, 타입을 가능한 값의 집합으로 볼 수 있습니다. 즉, 타입의 모든 값은 집합의 요소라고 생각할 수 있으며, 이는 집합의 정의에 따라 요소가 속한 집합과 비교 가능한 타입을 만듭니다.
상상해보세요...
number
타입은 가능한 모든 숫자의 무한 집합입니다.string
타입은 가능한 모든 문자 순열의 무한 집합입니다.object
타입은 객체가 취할 수 있는 모든 모양의 무한 집합입니다. JavaScript에서 객체에는 함수, 배열, 날짜, 정규식 등이 포함됩니다.undefined
, null
그리고 boolean
타입을 고려하십시오. 이 타입은 모두 제한된 수의 요소를 보유하는 집합입니다.상상해보세요...
undefined
을 포함하는 유한 집합으로서의 undefined
타입null
을 포함하는 유한 집합으로서의 null
타입true
와 false
두 값을 포함하는 유한 집합으로서의 boolean
타입.그 외의 유한 집합은 문자열 리터럴타입과 문자열 리터럴 유니언타입이 있습니다. 첫 번째는 하나의 사용자 지정 문자열 리터럴을 포함하는 집합입니다. 두 번째는 소수의 사용자 지정 문자열 리터럴을 포함하는 집합입니다. 각 집합은 가능한 모든 문자열 집합의 적절한 부분 집합입니다.
// 둘 다 true, 문자열 리터럴 ⊂ 문자열
type W = 'a' extends string ? true : false;
type X = 'a' | 'b' extends string ? true : false;
// true, 문자열 리터럴 ⊆ 같은 문자열 리터럴
type Y = 'a' extends 'a' ? true : false;
// true, 문자열 ⊆ 문자열
type Z = string extends string ? true : false;
조건부 타입의 extends
가 다음과 같은 TypeScript와 어떻게 동일한지 확인해보세요.
이는 일반적인 제약 조건의 extends
에도 적용됩니다.
// 제약 조건: T ⊂ 문자열 또는 T ⊆ 문자열
declare function myFunc<T extends string>(arg: T): T[]
그리고 인터페이스 선언에서는 다음과 같습니다.
interface Person {
name: string;
}
interface Employee extends Person {
salary: number;
}
// true, Person ⊂ object
type Q = Person extends object ? true : false;
// true, Employee ⊂ Person
type R = Employee extends Person ? true : false;
object
는 가능한 모든 객체 모양의 집합입니다. 인터페이스는 속성이 인터페이스와 일치하는 모든 객체 모양의 집합이므로 지정된 모든 인터페이스는 객체 타입의 적절한 부분 집합입니다.
그리고 자식 인터페이스가 부모 인터페이스를 extends
하면 자식 인터페이스는 속성이 부모 인터페이스와 일치하는 가능한 모든 객체 모양의 집합입니다. 따라서 자식 인터페이스는 부모 인터페이스의 적절한 부분 집합이며, 부모 인터페이스는 그 자체로서 적절한 부분 집합입니다.
또한 타입이 다른 타입의 적절한 부분 집합인 경우, 해당 관계는 할당시의 호환성을 암시합니다.
let myString: string = 'myString';
let myStringLiteral: 'only' | 'specific' | 'strings' = 'only';
// 둘 다 할당 가능, 문자열 ⊂ 문자열, 문자열 리터럴 ⊂ 문자열
myString = 'myNewString';
myString = myStringLiteral;
// 할당할 수 없음, 문자열 ⊄ 문자열 리터럴
myStringLiteral = myString;
이들 및 기타 컴파일러 동작은 집합론으로 설명됩니다.
타입을 집합으로 생각하면 다음 사항을 추론하는 데 도움이 될 수 있습니다.
1. 할당 중 타입 호환성.
2. 타입 연산자를 사용한 타입 생성.
3. 조건부 타입 해결.
할당은 변수를 라벨로 한 특정 메모리 위치에 값을 저장합니다. 값과 변수는 모두 입력되므로 할당 가능성(즉, 할당 호환성)은 할당되는 값의 타입과 수신자 변수의 타입에 따라 달라집니다.
두 타입이 동일하면 할당이 성공합니다.
let a: number;
a = 123; // 성공, 숫자는 숫자에 할당 가능
그러나 두 타입이 동일하지 않은 경우 할당이 성공하려면 타입 변환이 발생해야 합니다. 한 타입의 값을 다른 타입의 변수에 할당하면 타입 캐스팅이 됩니다. 즉, 값의 타입이 변수의 다른 타입이 됩니다.
타입 캐스팅은 종종 업캐스팅의 형태를 취합니다. 값의 타입을 변수에서 더 포괄적인 타입으로 확장합니다. 예를 들어 문자열 리터럴은 문자열이 됩니다.
let myString: string = 'myString';
let myStringLiteral: 'only' | 'specific' | 'strings' = 'only';
myString = myStringLiteral; // 업캐스팅 성공, 할당 가능
업캐스팅은 하위타입을 상위타입으로 변환합니다. 즉 적절한 하위 집합을 상위 집합으로 변환합니다. TypeScript는 Type-Safe이기 때문에 이 변환을 허용합니다. 집합이 다른 집합의 적절한 하위 집합일 경우 작은 집합의 모든 요소도 큰 집합의 요소가 됩니다.
반면 다운캐스팅은 일반적으로 허용되지 않습니다. 타입의 안전성을 보장하기 위해 더 큰 집합의 요소가 더 작은 집합의 요소이기도 하다고 선언할 수 없습니다. 확실히 알 수 없습니다. 그리고 두 집합이 같으면 두 타입이 동일하므로 타입 캐스팅이 필요하지 않습니다.
위의 할당을 반대로 하면 문자열을 문자열 리터럴로 다운캐스팅하는 것이 허용되지 않음을 알 수 있습니다.
let myString: string = 'myString';
let myStringLiteral: 'only' | 'specific' | 'strings' = 'only';
myStringLiteral = myString; // 다운캐스팅 실패, 할당 불가
이 논리에 따라 두 가지 특수한 경우를 제외하고 할당 중에 허용되는 유형 변환을 예측할 수 있습니다.
할당 가능성 측면에서 never
는 이 점에서 특별합니다...
never
은 다른 모든 타입에 할당할 수 있습니다.never
에 할당할 수 있는 타입은 없습니다.즉, 모든 타입은 never
의 수신 측에 있을 수 있으며 never
자체는 다른 타입의 수신 측에 있을 수 없습니다. 즉, never
에서 다른 타입으로의 업캐스팅은 가능하지만(아래의 상자 주석 참조), never
에서 다른 타입으로의 다운캐스팅은 타입 안전을 위해 허용되지 않습니다. 따라서 never
는 bottom type이라고 하고 집합론에서 ⊥로 표기합니다.
집합론 용어에서 말하자면, never
는 어떤 요소도 구성원이 될 수 없고 다른 어떤 집합도 부분 집합이 될 수 없는 집합입니다. never
는 빈 집합, 아무것도 포함하지 않는 집합입니다.
const a: never = 1; // 다운캐스팅 실패, 할당 불가
유효한
never
지정에 대한 예제가 없습니다.
never
는 항상 더 포괄적인 타입으로 확장될 수 있지만, 정의상never
의 값은 절대로 발생할 수 없기 때문에 실제로는 다른 타입에 할당되는never
에 대한 예를 제공할 수 없습니다. -never
타입의 실제 값을 할당에 사용할 수 없습니다.
그러나 실제로never
값을 할당에 사용할 수 없는 경우never
를 다른 유형에 할당할 수 있다는 것은 무슨 의미일까요? 이 답변을 참조해주세요.
그 반대의 경우는 unknown
으로, 주로 사용 전에 확인해야하는 타입의 값을 입력하는 데 사용됩니다. 즉, JSON.parse()
는 이상적으로 unknown
을 반환해야 합니다. TypeScript는 안전하게 사용하기 전에 모든 unknown
값이 어떤 유형인지 알아내도록 합니다.
let a: unknown;
a.toUpperCase(); // 계속 unknown, 허용되지 않음
if (typeof a === 'string') {
a.toUpperCase(); // 문자열로 좁혀짐, 허용됨
}
할당성 측면에서 unknown
은 이 점에서 특별합니다...
unknown
은 모든 타입을 할당받을 수 있습니다.unknown
은 다른 타입에는 할당할 수 없습니다.unknown
은 모든 타입의 수신 측에 있을 수 있으며 어떤 타입도 unknown
의 수신 측에 있을 수 없습니다. 즉, 다른 타입에서 unknown
으로의 업캐스팅이 가능하며(unknown
은 호출자가 값의 타입 확인을 요구하기 위해 존재하기 때문에 거의 유용하지 않음) unknown
에서 다운캐스팅은 타입 안전을 위해 허용되지 않습니다.
unknown
은 사용하기 전에 타입을 확인("정제")해야 하기 때문에 unknown
은 잠재적으로 모든 타입입니다. 모든 타입은 unknown
이라는 우산 아래에 있습니다. 따라서 unknown
은 top type이라고 하고 집합론에서 ⊤로 표기합니다.
let x: unknown = 'a'; // 업캐스팅 성공, 할당
let a: unknown
const b: string = a; // 다운캐스팅 실패, 할당 불가
집합론 용어에서 말하자면, unknown
은 다른 모든 유형의 상위 집합입니다. 모든 요소가 구성원이고 모든 집합이 하위 집합인 집합입니다. 따라서 unknown
은 모든 것을 포함하는 보편적인 집합입니다.
요약하자면, never
및 unknown
은 다운캐스팅이 허용되지 않는다는 점에서 다른 유형과 유사하지만 never
에서 업캐스팅 및 unknown
으로 업캐스팅이 모두 가능하지만 실제로는 거의 발생하지 않는다는 점에서 다른 유형과 다릅니다.
탈출구로서의
any
이상하게도
any
는never
와unknown
의 합성어입니다.any
는never
와 마찬가지로 모든 타입에 할당할 수 있고 모든 타입은unknown
과 마찬가지로any
에 할당할 수 있습니다. 두 가지 상반되는 것의 혼합으로서any
는 집합 이론에서 동등한 것이 없으며 TypeScript의 할당 가능성 규칙을 비활성화하는 탈출구로 가장 잘 보입니다.
집합 연산자를 사용하여 기존 집합을 새로운 집합으로 결합할 수 있습니다.
그래픽으로:
이들 네 가지의 집합 연산자 중 TypeScript는 두 가지를 유형 연산자로 구현합니다.
|
결합을 위해&
교차용으로|
로 결합하는 것은 두 입력 타입으로 구성된 더 넓고 포괄적인 타입을 만드는 것을 의미하는 반면 &
와 교차한다는 것은 두 입력 타입이 공유하는 요소로만 구성된 더 작고 더 제한적인 타입을 만드는 것을 의미합니다.
유형 연산자로서 |
및 &
는 해당 집합에 속하는 요소(값)가 아닌 유형(집합)에서 작동합니다. 유형 연산자를 유형을 입력으로 받아들이고 다른 유형을 출력으로 반환하는 함수로 생각하세요.
원형을 조작하는 경우|
및 &
는 예상대로 작동합니다.
type StringOrNumber = string | number;
// string | number → 문자열과 숫자 모두 허용됨
type StringAndNumber = string & number;
// never → 어떤 유형도 허용되지 않음
다만, 인터페이스에서 동작하는 경우는 |
및 &
는 반직관적으로 행동하는 것처럼 보입니다.
다음 예를 생각해 보겠습니다.
interface ICat {
eat(): void;
meow(): void;
}
interface IDog {
eat(): void;
bark(): void;
}
declare function Pet(): ICat | IDog;
const pet = new Pet();
pet.eat(); // 성공
pet.meow(); // 실패
pet.bark(); // 실패
유니언 타입의 |
는 일반적으로 "A 또는 B가 허용됨"을 의미하는 것으로 간주됩니다. 이는 boolean 연산자 ||
가 표현식에서 OR
을 의미한다는 사실과 대략 일치합니다. 그러나 OR
측면에서 인터페이스의 결합을 생각하면 오해의 소지가 있습니다.
출력 유형 ICat | IDog
는 "ICat
의 메서드 또는 IDog
의 메서드"을 허용하므로 출력 유형 ICat | IDog
는 ICat
메서드 또는 IDog
메서드를 사용하여 객체를 받아들입니다.
하지만 그것은 컴파일러가 허용하는 것이 아닙니다. 두 인터페이스를 통합하면 더 큰 집합, 즉 입력 집합 간에 공통된 메소드만 있는 인터페이스가 생성됩니다. 다시 말해, 유니온 타입 ICat | IDog
는 입력 집합의 공유 요소로 구성된 새로운 출력 집합입니다.
그리고 그 반대는 교차 유형에도 적용됩니다. 교차 유형의 &
는 일반적으로 "하나와 다른 것"을 의미하는 것으로 간주됩니다. 이는 boolean 연산자 &&
가 표현식에서 AND
를 의미한다는 사실과 일치합니다. 그러나 AND
측면에서 인터페이스의 교차점을 생각하는 것도 오해의 소지가 있습니다.
다음 예를 생각해 보겠습니다.
interface ICat {
eat(): void;
meow(): void;
}
interface IDog {
eat(): void;
bark(): void;
}
declare function Pet(): ICat & IDog;
const pet = new Pet();
pet.eat(); // 성공
pet.meow(); // 성공
pet.bark(); // 성공
두 인터페이스를 교차하면 더 작은 집합, 즉 더 많은 메소드(두 입력 집합의 모든 메소드)가 있는 인터페이스가 생성됩니다.
요약하자면, 인터페이스를 유니온화할 때 OR
의 관점에서 생각하면 해석이 흐려질 수 있지만 더 넓은 출력 세트를 시각화하면 해석이 명확해질 수 있습니다. 그리고 인터페이스를 교차할 때 AND
의 관점에서 생각하면 해석이 흐려질 수 있지만 더 좁은 출력 세트를 시각화하면 해석이 명확해질 수 있습니다.
그러나 인터페이스를 결합하고 교차할 때 우리의 예상이 뒤바뀌는 이유는 무엇일까요?
object
타입은 가능한 모든 객체 모양의 무한 집합이고, 인터페이스는 특정 속성을 가진 가능한 모든 객체 모양의 무한 집합입니다. 인터페이스는 object
집합의 하위 집합입니다. 가능한 모든 객체 모양의 유니버스에서 인터페이스와 일치하는 속성을 가진 객체 모양이 할당될 수 있습니다.
let a: object;
a = { z: 1 }; // { z: number } 는 객체에 할당 가능
인터페이스는 객체의 모양을 설명하기 때문에, 인터페이스에 더 많은 속성을 추가할수록 일치하는 객체 모양 수가 적어지므로 가능한 값의 집합이 작아집니다. 인터페이스에 속성을 추가하면 해당 속성이 나타내는 집합이 축소되고 그 반대도 마찬가지입니다.
interface Person {
name: string;
age: number;
isMarried: boolean;
}
2개의 인터페이스를 결합할 때는 부분적으로 중복되는 완전히 음영 처리된 두 집합을 시각화합니다. 결합할 때 일치하는 유형을 허용하는 출력 유형을 생성합니다...
가능한 모든 객체 모양의 유니버스에서 이들 3개의 객체 모양이 출력 타입에 할당 가능하기 때문에 출력 타입은 2개의 입력 자체보다 넓고 포괄적입니다. 인터페이스를 OR
로 합집합화하는 것은 두 가지가 겹치는 부분을 고려해야 한다는 점을 기억할 때만 의미가 있습니다.
interface A {
a: 1;
}
interface B {
b: 1;
}
const x: A | B = { a: 1 }; // 성공
const y: A | B = { b: 1 }; // 성공
const z: A | B = { a: 1, b: 1 }; // 성공, 중복 할당 가능
string | number
와 같은 원시타입의 조합도 중첩을 발생시키지만, 동시에 두 종류인 원시타입은 없으므로 원시의 중첩에 할당할 수 있는 것은 없습니다. 우리는 이 경우를 간과하는 경향이 있기 때문에 boolean OR
이 모든 유형의 조합에 대해 생각하는 정확한 방법이라고 기본적으로 생각합니다. 이는 원시적이지 않은 것으로 작동할 때 우리는 길을 잃을 수 있습니다.
반대로, 두 개의 인터페이스를 교차할 때, 부분적으로 겹치는 두 집합을 시각화하고, 겹치는 부분에서만 음영 처리합니다. 교차할 때 중첩과 일치하는 유형, 즉 공유된 부분만 허용하는 출력 유형을 만듭니다.
interface A {
a: 1;
}
interface B {
b: 1;
}
const x: A & B = { a: 1 }; // 실패
const y: A & B = { b: 1 }; // 실패
const z: A & B = { a: 1, b: 1 }; // 성공
string & number
와 같은 원시적인 요소의 교차점은 어떤 원시적인 요소도 다른 요소와 공유할 수 없기 때문에 항상 never
를 생성합니다. 그러나 인터페이스는 object
의 하위 집합이므로 인터페이스의 교차점은 항상 두 입력 객체 모양을 동시에 만족시킬 수 있는 인터페이스를 생성합니다. 교차된 인터페이스에 공통 속성이 없는 경우에도 가능한 모든 객체 모양의 조각이라는 공통점이 있습니다.
그리고 다시, 원시를 교차하는 직관을 비원시를 교차하는 직관으로 전달하면 boolean AND
용어로 생각하게 되므로, 위의 x
와 y
가 그렇지 않을 때 성공해야 한다고 잘못 가정할 위험이 있습니다.
누적 효과:
서로 다른 인터페이스를 통합하면 중복으로 인해 속성이 누적됩니다. 서로 다른 인터페이스를 교차하면 출력 유형에 속성이 누적됩니다. 인터페이스가 다른 인터페이스를
extends
한다고 선언하면 자식 인터페이스가 속성을 축적합니다.세 가지 경우 모두 이 누적 효과는 동일한 인터페이스의 개별 선언이 각각의 속성을 누적하는 통합 인터페이스를 생성하는 인터페이스 선언 병합의 효과와 유사합니다.
집합론에서는 집합의 모든 원소에 대해 보편적으로 참인 방정식이 있습니다.
집합에서 지원하는 네 가지 연산자 모두에 대해 총 12가지 법칙이 있지만 TypeScript는 |
와&
만 구현하므로 12가지 법칙 중 일부만 집합과 유형 모두에 적용됩니다. 이 중에서, 항등법칙과 멱등법칙이라는 두 쌍의 법칙은 특히 조건부 유형을 이해하는 데 유용합니다.
공집합과 합집합의 집합은 스스로 해결됩니다. 범용 집합과 교차하는 집합도 자체적으로 해결됩니다. 특수 집합은 TypeScript와 마찬가지로 항등법칙에 따라 축소됩니다:
type A = string | never; // 문자열로 확인
type B = string & unknown; // 문자열로 확인
자신과 합집합의 집합은 스스로 해결됩니다. 자신과 교차하는 집합도 자신으로 해결됩니다. 집합이든 유형이든 중복 항목은 멱등성 법칙에 따라 필터링됩니다:
type A = string | string; // 문자열로 확인
type B = string & string; // 문자열로 확인
항등법과 멱등법은 조건부 유형과 어떤 관련이 있을까요?
유형이 집합인 경우 조건부 유형의 조건은 부분 집합 검사에 해당합니다. 집합이 다른 집합의 (적절한) 부분집합인가요? 그렇다면 지정된 유형을 수신자 유형에 할당할 수 있습니다. 체크의 초점으로 우리가 묻는 이 주어진 유형을 체크된 타입이라고 합니다.
type R = 'a' extends string ? true : false; // true
type S = 'a' | 'b' extends number ? true : false; // false
type T = { a: 1; b: 2 } extends { a: 1 } ? true : false; // true
type U = { a: 1 } extends { a: 1; b: 2 } ? true : false; // false
체크된 타입이 구체적인 경우 조건부 유형 검사는 간단합니다. 하지만 체크된 타입이 제네릭이라면 어떨까요? 제네릭을 조건부 유형에 도입할 때 일반적으로 확인된 유형의 제네릭에 액세스할 수 있도록 하기 위해, 종종 입력을 필터링하거나 출력을 변환합니다:
type X<T> = T extends OtherType ? T : never;
type Y<T> = T extends OtherType ? T[] : never;
type Z<T> = T extends OtherType ? { a: T } : never;
// 검사중인 제네릭이 출력에 사용되고 있습니다.
유니온 타입을 체크된 제네릭으로 넘길 때 조건부 타입 해결은 더 이상 간단하지 않고 해석에 개방적이게됩니다.
우리가 조건부 타입의 제네릭에 유니온을 전달하는 것은, 무슨 의미일까요...?
첫 번째 해석은 분배 조건부(distributive conditional type) 유형입니다. 여기에서 체크는 유니온의 각 요소에게 분배됩니다. 즉, 유니온 요소별로 질문을 하고 유니온 요소별 답변을 기준으로 유형을 결정합니다. 이것은 유니온이 전달되는 체크된 제네릭이 있는 조건부 유형에 대한 TypeScript의 기본 해결 전략입니다.
// 분배 조건부 유형
type ToArrayDist<T> = T extends unknown ? T[] : never;
// 분배 조건부 유형을 호출
type R = ToArrayDist<string | number>; // string[] | number[]
// 분배를 자세히 설명
type R =
| (string extends unknown ? string[] : never)
| (number extends unknown ? number[] : never);
// 타입을 확인하고 검사가 진행된 후 - 최종 결과
type R = string[] | number[];
차이점 유의:
분배 조건부 유형(distributive conditional type)유니온에 대한 체크를 분배하여 유니언 요소당 하나의 해결된 유형을 생성합니다. 따라서 분배 조건부 유형은 이 섹션의 시작 부분에 나와있는 것처럼 세 개의 서로 다른 집합에 대한 교집합과 합집합을 재분배하는 집합 분배 법칙(set distributive laws)과 관련이 없습니다.
두 번째 해석은 전체적으로 유니온에 대해 작동하는 비분배 조건부 유형(non-distributive conditional type)입니다. 분배가 기본 동작이므로 분배를 비활성화하려면 조건의 두 가지 유형을 각각 대괄호로 묶어야 합니다. 비분배는 유니온이 전달되는 검사된 제네릭이 있는 조건부 유형에 대한 TypeScript의 대체 해결 전략입니다.
// 비분배 조건부 유형
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
// 비분배를 조건부 유형을 호출
type R = ToArrayNonDist<string | number>;
// 타입을 확인하고 검사가 진행된 후 - 최종 결과
type R = (string | number)[];
위의 예제에서 조건은 항상 참(즉, 잘못된 분기는 도달하지 않음)이므로 분배 효과에 집중할 수 있습니다. 항상 참인 조건을 달성하는 다른 방법으로는 T extend any
와 T extend T
가 있습니다. 다음 조건은 분배 도우미(distributive helpers)를 만드는 데 유용합니다:
// 분배 조건부 유형
type GetKeys<T> = T extends T ? keyof T : never;
// 분배 조건부 유형을 호출
type R = GetKeys<{ a: 1; b: 2 } | { c: 3 }>;
// 분배를 자세히 설명
type R
| { a: 1; b: 2 } extends { a: 1; b: 2 } ? keyof { a: 1; b: 2 } : never
| { c: 3 } extends { c: 3 } ? keyof { c: 3 } : never;
// 타입을 확인하고 검사가 진행된 후
type R = keyof { a: 1; b: 2 } | keyof { c: 3 };
// keyof 연산자가 적용된 후 - 최종 결과
type R = 'a' | 'b' | 'c';
이와는 대조적으로, true 및 false 분기에 모두 도달할 수 있는 분배 조건부 유형에서는 각 구성 요소의 해결이 출력 유니온에서 함께 연결된 never
인스턴스와 중복 유형을 표시하도록 바인딩됩니다. 중복 유형 및 never
인스턴스가 발생하면 TypeScript는 항등법칙 및 멱등법칙을 적용하여 확인된 유형의 결합을 최소 표현으로 필터링하고 축소합니다:
// 분배 조건부 유형
type NonNullable<T> = T extends null | undefined ? never : T;
// 분배 조건부 유형을 호출
type R = NonNullable<string | string | string[] | null | undefined>;
// 분배를 자세히 설명
type R =
| (string extends null | undefined ? never : string)
| (string extends null | undefined ? never : string)
| (string[] extends null | undefined ? never : string[])
| (null extends null | undefined ? never : null)
| (undefined extends null | undefined ? never : undefined);
// 타입을 확인하고 검사가 진행된 후
type R = string | string | string[] | never | never;
// 멱등법칙으로 never 중복 제거
type R = string | string | string[];
// 멱등법으로 조합 중복 제거 - 최종 결과
type R = string | string[];
마지막으로 조건부 유형이 분산성을 트리거하려면 확인된 제네릭이 확장의 왼쪽에 있어야 합니다. 즉, 다른 제네릭으로 전달되거나 검사 중에 변경되지 않아야 합니다.
// 분배, 자체적으로 확인된 제네릭
type R<T> = T extends OtherType ? true : false;
// 비분배, 자체적으로 검사된 제네릭이 아님
type R<T> = SomeType<T> extends OtherType ? true : false;