[번역] TypeScript 및 집합 이론

yeeeD·2023년 2월 22일
0

Article

목록 보기
1/1

원문: https://ivov.dev/notes/typescript-and-set-theory

프레젠테이션:

서론

집합론은 집합(즉, 요소의 집합)에 전념하는 수학의 한 분야입니다. 포함된 요소를 열거하여 집합을 정의할 수 있습니다.

{a,b,c}\{ a, b, c \}

또는 어떤 요소가 집합에 속하는지 결정하는 규칙을 기술합니다.

{xSP(x)}\{ x \in S | P(x)\}

이 표기법은 "PPxx 인 집합 SS 에 속하는 모든 요소 xx 의 집합" 이라고 읽습니다. 이 함수는 predicate라고 불리며, 그 목적은 집합에 속하는 요소를 선택하는 것입니다. predicate를 filter() 함수에 대한 콜백으로 생각하세요. 입력의 항목이 우리가 만들고 있는 집합에 속하는 경우 true를 리턴합니다. PP 는 집합의 구성원 자격을 갖추기 위해 모든 요소가 충족해야 하는 제약 조건입니다.

집합을 정의하면 주어진 요소와 집합의 관계를 설명할 수 있습니다. 요소는 세트의 멤버일 수도 있고 아닐 수도 있습니다 (즉, 다음과 같은 집합에 속할 수도 있습니다).

a{a,b,c}a \in \{ a, b, c \}
d{a,b,c}d \notin \{ a, b, c \}

집합과 다른 집합의 관계를 설명할 수도 있습니다. 집합은 첫 번째 집합의 모든 요소가 두 번째 집합의 요소이기도 한 경우에만 다른 집합의 부분 집합(즉, 포함)입니다. 즉, 하나의 집합의 모든 항목이 다른 집합에 존재할 때 첫 번째는 두 번째의 부분 집합입니다.
또한 첫 번째 집합에는 존재하지 않는 요소가 두 번째 집합에도 하나 이상 있는 경우 첫 번째(작은) 집합은 첫 번째의 상위 집합인 두 번째(큰) 집합의 적절한 부분 집합입니다.

{a,b}{a,b}\{ a, b \} \subseteq \{ a, b \}
{a,b}{a,b,c}\{ a, b \} \subset \{ a, b, c \}
ABx,  if  xA  then  xBA \subset B \Longleftrightarrow \forall x, \;\mathrm{if}\;x \in A\;\mathrm{then}\;x \in B

함축적으로, 적절한 부분집합이 아닌 다른 집합의 부분집합인 집합은 다른 집합과만 같을 수 있습니다. 집합은 전자의 모든 요소가 후자의 구성원이고 후자의 모든 요소가 전자의 구성원인 경우에만 다른 집합과 같습니다. 집합의 순서와 중복 무시 — 두 집합은 동일한 요소를 포함하는 경우에만 동일합니다.

{a,b}={b,a}\{a, b\} = \{b, a\}
A=BAB  and  BAA = B \Longleftrightarrow A \subseteq B \;\mathrm{and}\;B \subseteq A

그래픽으로:

반대로 하위 집합이 아닌 것을 고려합니다. 첫 번째 집합에 두 번째 집합의 구성원이 아닌 요소가 적어도 1개 존재하는 경우 집합은 다른 집합의 부분 집합이 아닙니다. 이것들은 교집합입니다.

{a,d}⊄{a,b,c}\{ a, d \} \not\subset \{ a, b, c\}
A⊄Bx,  such  thatxA  and  x∉BA \not\subset B \Longleftrightarrow \exists x, \;\mathrm{such\;that}\, x \in A \;\mathrm{and}\; x \not\in B

그래픽으로:

마지막으로, 두 가지 특별한 경우입니다. Ø로 표시되는 공집합은 아무것도 포함하지 않는 집합입니다. 예를 들어, 서로소 집합 사이에 공통 요소를 포함하는 집합입니다. 정확히 말하면 공집합은 아무것도 아닌 것이 아니라 아무것도 담지 않는 그릇입니다.

그리고 그 반대는 U로 표시되는 합집합입니다. 모든 집합이 부분 집합이고 모든 요소가 구성원인 집합입니다. 수학에서 합집합은 논의의 경계를 나타냅니다.

추가 정보:

다음 섹션에 대한 간략한 소개입니다. 집합론에 대해 더 알아보려면:

집합으로서의 타입

집합론은 TypeScript의 유형에 대한 추론을 위한 정신적 모델을 제공합니다. 집합론의 관점을 통해, 타입을 가능한 값의 집합으로 볼 수 있습니다. 즉, 타입의 모든 값은 집합의 요소라고 생각할 수 있으며, 이는 집합의 정의에 따라 요소가 속한 집합과 비교 가능한 타입을 만듭니다.

상상해보세요...

  • number타입은 가능한 모든 숫자의 무한 집합입니다.
  • string타입은 가능한 모든 문자 순열의 무한 집합입니다.
  • object타입은 객체가 취할 수 있는 모든 모양의 무한 집합입니다. JavaScript에서 객체에는 함수, 배열, 날짜, 정규식 등이 포함됩니다.

    모든 타입의 집합이 무한하지는 않습니다. undefined, null 그리고 boolean 타입을 고려하십시오. 이 타입은 모두 제한된 수의 요소를 보유하는 집합입니다.

상상해보세요...

  • 단일 값 undefined을 포함하는 유한 집합으로서의 undefined타입
  • 단일 값 null을 포함하는 유한 집합으로서의 null타입
  • truefalse 두 값을 포함하는 유한 집합으로서의 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와 어떻게 동일한지 확인해보세요.

  •   \subset \;(적절한 부분 집합, 즉 A의 모든 요소는 B에 있고 B에는 추가 요소가 있습니다.)
  •   \subseteq \;(하위 집합, 즉 A의 모든 요소는 B에 있고 B에는 추가 요소가 없습니다.)

이는 일반적인 제약 조건의 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. 조건부 타입 해결.

파트 1: 할당 가능성

할당은 변수를 라벨로 한 특정 메모리 위치에 값을 저장합니다. 값과 변수는 모두 입력되므로 할당 가능성(즉, 할당 호환성)은 할당되는 값의 타입과 수신자 변수의 타입에 따라 달라집니다.

두 타입이 동일하면 할당이 성공합니다.

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에서 다른 타입으로의 다운캐스팅은 타입 안전을 위해 허용되지 않습니다. 따라서 neverbottom 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이라는 우산 아래에 있습니다. 따라서 unknowntop type이라고 하고 집합론에서 ⊤로 표기합니다.

let x: unknown = 'a'; // 업캐스팅 성공, 할당

let a: unknown
const b: string = a; // 다운캐스팅 실패, 할당 불가

집합론 용어에서 말하자면, unknown은 다른 모든 유형의 상위 집합입니다. 모든 요소가 구성원이고 모든 집합이 하위 집합인 집합입니다. 따라서 unknown은 모든 것을 포함하는 보편적인 집합입니다.

요약하자면, neverunknown은 다운캐스팅이 허용되지 않는다는 점에서 다른 유형과 유사하지만 never에서 업캐스팅 및 unknown으로 업캐스팅이 모두 가능하지만 실제로는 거의 발생하지 않는다는 점에서 다른 유형과 다릅니다.

탈출구로서의 any

이상하게도 anyneverunknown의 합성어입니다. anynever와 마찬가지로 모든 타입에 할당할 수 있고 모든 타입은 unknown과 마찬가지로 any에 할당할 수 있습니다. 두 가지 상반되는 것의 혼합으로서 any는 집합 이론에서 동등한 것이 없으며 TypeScript의 할당 가능성 규칙을 비활성화하는 탈출구로 가장 잘 보입니다.

파트 2: 타입 생성

집합 연산자를 사용하여 기존 집합을 새로운 집합으로 결합할 수 있습니다.

  • A와 B의 합집합은 적어도 A나 B에 속하는 모든 요소의 집합입니다.
  • A와 B의 교집합은 A와 B 모두에 있는 모든 요소의 집합입니다.
  • A - B의 차이는 A에 있고 B에 없는 모든 요소의 집합입니다.
  • A의 여집합은 A에 속하지 않고 U에 있는 모든 원소의 집합입니다.
AB={  xU    xA  or  xB  }A\cup B =\{\;x \in U\;|\;x \in A\;\mathrm{or}\;x \in B \;\}
AB={  xU    xA  and  xB  }A \cap B =\{\;x \in U\;|\;x \in A\;\mathrm{and}\;x \in B \;\}
AB={  xU    xA  and  x∉B  }A - B =\{\;x \in U\;|\;x \in A\;\mathrm{and}\;x \not\in B \;\}
A={  xU    x∉A  }\overline{A} = \{\;x \in U\;|\;x \not\in A \;\}

그래픽으로:

이들 네 가지의 집합 연산자 중 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 | IDogICat 메서드 또는 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개의 인터페이스를 결합할 때는 부분적으로 중복되는 완전히 음영 처리된 두 집합을 시각화합니다. 결합할 때 일치하는 유형을 허용하는 출력 유형을 생성합니다...

  • 1개의 입력 타입
  • 다른 입력 타입
  • 둘 다

가능한 모든 객체 모양의 유니버스에서 이들 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 용어로 생각하게 되므로, 위의 xy가 그렇지 않을 때 성공해야 한다고 잘못 가정할 위험이 있습니다.

누적 효과:

서로 다른 인터페이스를 통합하면 중복으로 인해 속성이 누적됩니다. 서로 다른 인터페이스를 교차하면 출력 유형에 속성이 누적됩니다. 인터페이스가 다른 인터페이스를 extends한다고 선언하면 자식 인터페이스가 속성을 축적합니다.

세 가지 경우 모두 이 누적 효과는 동일한 인터페이스의 개별 선언이 각각의 속성을 누적하는 통합 인터페이스를 생성하는 인터페이스 선언 병합의 효과와 유사합니다.

파트 3: 조건부 유형의 해결

집합론에서는 집합의 모든 원소에 대해 보편적으로 참인 방정식이 있습니다.

AB=BAAB=BA교환법칙A \cup B = B \cup A \\ A \cap B = B \cap A \\ {\scriptstyle\text{교환법칙}}
(AB)C=A(BC)(AB)C=A(BC)결합법칙(A \cup B) \cup C = A \cup (B \cup C) \\ (A \cap B) \cap C = A \cap (B \cap C) \\ {\scriptstyle\text{결합법칙}}
(A(BC)=(AB)(AC)(A(BC)=(AB)(AC)분배법칙(A \cup (B \cap C) = (A \cup B) \cap (A \cup C) \\ (A \cap (B \cup C) = (A \cap B) \cup (A \cap C) \\ {\scriptstyle\text{분배법칙}}

집합에서 지원하는 네 가지 연산자 모두에 대해 총 12가지 법칙이 있지만 TypeScript는 |&만 구현하므로 12가지 법칙 중 일부만 집합과 유형 모두에 적용됩니다. 이 중에서, 항등법칙멱등법칙이라는 두 쌍의 법칙은 특히 조건부 유형을 이해하는 데 유용합니다.

A=AAU=A항등법칙A \cup \varnothing = A \\ A \cap U = A \\ {\scriptstyle\text{항등법칙}}

공집합과 합집합의 집합은 스스로 해결됩니다. 범용 집합과 교차하는 집합도 자체적으로 해결됩니다. 특수 집합은 TypeScript와 마찬가지로 항등법칙에 따라 축소됩니다:

type A = string | never; // 문자열로 확인
type B = string & unknown; // 문자열로 확인
AA=AAA=A멱등법칙A \cup A = A \\ A \cap A = A \\ {\scriptstyle\text{멱등법칙}}

자신과 합집합의 집합은 스스로 해결됩니다. 자신과 교차하는 집합도 자신으로 해결됩니다. 집합이든 유형이든 중복 항목은 멱등성 법칙에 따라 필터링됩니다:

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 anyT 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;
profile
어제 모른 것은 오늘 알면 된다

0개의 댓글