본문으로 건너뛰기

타입 별명과 리터럴 타입_TypeScript 노트 10

무료2019-03-10#TypeScript#可辨识联合#narrow this type#TypeScript Type Aliases#TypeScript Literal Types#type assertions and type guards

TS 에서의 대수 데이터 타입의 유래

一.타입 별명

type PersonName = string;
type PhoneNumber = string;
type PhoneBookItem = [PersonName, PhoneNumber];
type PhoneBook = PhoneBookItem[];

let book: PhoneBook = [
  ['Lily', '1234'],
  ['Jean', '1234']
];

type 키워드는 기존 타입에 별명을 생성할 수 있어, 그 가독성을 향상시킵니다

인터페이스와 타입 별명

타입은 형식적으로 인터페이스와 조금 비슷하며, 둘 다 타입 파라미터를 서포트하고, 자신을 인용할 수 있습니다. 예를 들어:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

interface ITree<T> { 
  value: T;
  left: ITree<T>;
  right: ITree<T>;
}

그러나 본질적인 차이가 존재합니다:

  • 타입 별명은 새로운 타입을 생성하지 않으며, 인터페이스는 새로운 타입을 정의

  • 임의의 타입에 별명을 붙이는 것은 허가되지만, 임의의 타입에 그것과 동등한 인터페이스를 정의하는 것은 불가능 (예를 들어 기초 타입)

  • 타입 별명을 상속 또는 구현할 수 없지만 (다른 타입을 확장 또는 구현할 수도 없음), 인터페이스는 가능

  • 타입 별명은 여러 개의 타입을 조합하여 이름 붙은 타입으로 만들 수 있지만, 인터페이스는 이 종류의 조합 (교차, 연합 등) 을 기술할 수 없음

// 타입 조합, 인터페이스는 이 타입을 표현할 수 없음
type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
  name: string;
}
function findSomeone(people: LinkedList<Person>, name: string) {
  people.name;
  people.next.name;
  people.next.next.name;
  people.next.next.next.name;
}

应用场景上,二���区别如下:

  • 인터페이스: OOP 씬 (상속과 구현이 가능하고, 타입 계층 관계를 유지)

  • 타입 별명: 가독성을 추구하는 씬, 인터페이스가 기술할 수 없는 씬 (기초 타입, 교차 타입, 연합 타입 등)

二.리터럴 타입

2 종류의 리터럴 타입이 존재합니다: 문자열 리터럴 타입과 수치 리터럴 타입

문자열

문자열 리터럴도 타입 의미를 가집니다. 예를 들어:

let x: 'string';
// 오류 Type '"a"' is not assignable to type '"string"'.
x = 'a';
// 正确
x = 'string';

열거의 효과를 시뮬레이션하는 데 사용할 수 있습니다:

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out';
class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === 'ease-in') {}
    else if (easing === 'ease-out') {}
    else {
      // 자동으로"ease-in-out"타입으로 축窄
    }
  }
}

// 오류 Argument of type '"linear"' is not assignable to parameter of type 'Easing'.
new UIElement().animate(0, 0, 'linear');

서로 다른 문자열 리터럴은 서로 다른 구체적인 타입에 속합니다. 따라서, (필요하다면) 이렇게 오버로드할 수 있습니다:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element {
  return document.createElement(tagName);
}

수치

수치 리터럴도 마찬가지로 타입 의미를 가집니다:

// 주사위의 6 개 점수를 반환
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
  // ...
}

단순히 익명 수치 열거 처럼 보이며, 존재 필요성이 없어 보입니다

존재 의의

실제로, 리터럴 타입의 의의는 컴파일 시에 타입 정보를 결합하여「추론」할 수 있다는 것입니다. 예를 들어:

function foo(x: number) {
  // 오류 This condition will always return 'true' since the types '1' and '2' have no overlap.
  if (x !== 1 || x !== 2) { }
}

function bar(x: string) {
  // 오류 This condition will always return 'false' since the types '"1"' and '"2"' have no overlap.
  if (x === '1' && x === '2') { 
    //...
  }
}

이 타입 완전성 보충으로 인해, TypeScript 는 코드의 의미를 더욱 세밀하게「이해」(정적 분석) 할 수 있으며,进而에 몇 가지 그다지 직접적이지 않은 잠재적인 문제를 발견할 수 있습니다

Nevertheless, by pairing a type with it's unique inhabitant, singleton types bridge the gap between types and values.

三.열거와 리터럴 타입

연합 열거 라는 특수한 열거가 있다는 것을 알고 있습니다. 그 멤버도 타입 의미를 가집니다. 예를 들어:

// 연합 열거
enum E {
  Foo,
  Bar,
}

// 열거의 타입 의미
function f(x: E) {
  // 오류 This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
  if (x !== E.Foo || x !== E.Bar) {
    //...
  }
}

이것은 리터럴 타입 중의 예와 매우 비슷합니다:

function f(x: 'Foo' | 'Bar') {
  // 오류 This condition will always return 'true' since the types '"Foo"' and '"Bar"' have no overlap.
  if (x !== 'Foo' || x !== 'Bar') {
    //...
  }
}

P.S. 類比起見, 여기서 문자열 리터럴 연합 타입 ('Foo' | 'Bar') 으로 열거 E 를 시뮬레이션합니다. 실제로는 열거 E 는 수치 리터럴 연합 타입 (0 | 1) 과 동등합니다. 상세는 二。수치 열거 참조

타입 각도에서 보면, 연합 열거는 수치/문자열 리터럴로 구성된 열거입니다. 따라서 그 멤버도 타입 의미를 가집니다. 명칭 상에도 이 관계를 표현하고 있습니다: 연합 열거, 즉 수치/문자열 연합

P.S. 열거 멤버 타입과 수치/문자열 리터럴 타입은*단례 타입 (singleton types)*이라고도 불립니다:

Singleton types, types which have a unique inhabitant.

즉, 1 개의 단례 타입 아래에는 1 개의 값만 있습니다. 예를 들어 문자열 리터럴 타입 'Foo' 는 문자열 'Foo' 만 취값할 수 있습니다

四.식별 가능 연합

단례 타입, 연합 타입, 타입 가드, 타입 별명을 결합하여 일종의 패턴을建立할 수 있습니다. 식별 가능 연합 (discriminated unions) 이라고 불립니다

P.S. 식별 가능 연합은 태그 연합 (tagged unions) 또는 [대수 데이터 타입 (algebraic data types)](/articles/타입-haskell 노트 3/#articleHeader6) 이라고도 불립니다. 즉 연산 가능하고, 논리 추론이 가능한 타입

구체적으로는, 식별 가능 연합은 일반적으로 3 부분을 포함합니다:

  • 몇 개의 공공 단례 속성을 가진 타입——공공 단례 속성은 식별 가능한 특징 (또는 태그라고 부름)
  • 이러한 타입으로 구성된 연합을 가리키는 타입 별명——즉 연합
  • 공공 속성에 대한 타입 가드

공공 단례 속성의 타입을 구별하여 부모 타입을 축窄합니다. 예를 들어:

// 1. 몇 개의 공공 단례 속성 (kind) 을 가진 타입
interface Square {
    kind: "square";
    size: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
// 2. 연합 타입을 정의하고, 별명을 붙임
type Shape = Square | Circle;
// 3. 구체적으로 사용 (타입 가드)
function area(s: Shape) {
  switch (s.kind) {
    // 자동으로 Square 로 축窄
    case "square": return s.size * s.size;
    // 자동으로 Circle 로 축窄
    case "circle": return Math.PI * s.radius ** 2;
  }
}

[instanceof 타입 가드](/articles/조합 타입과 타입 가드-typescript 노트 9/#articleHeader6) 에 대한 일종의 보충입니다. 둘 다 복잡한 타입의 호환 관계를 검출하는 데 사용됩니다. 차이는 다음과 같습니다:

  • instanceof 타입 가드: 명확한 상속 관계를 가진 부모 자식 타입에 적용

  • 식별 가능 연합 타입 가드: 명확한 상속 관계가 없는 (런타임에 instanceof 로 상속 관계를 검출할 수 없는) 부모 자식 타입에 적용

완전성 체크

때때로 연합 타입의 모든 구성 타입을 완전히 커버하고 싶은 경우가 있습니다. 예를 들어:

type Shape = Square | Circle;
function area(s: Shape) {
  switch (s.kind) {
    case "square": return s.size * s.size;
    // 잠재 문제:"circle"을 누락
  }
}

never 타입을 통해 이 종류의 보장을 실현할 수 있습니다 (Never 타입이 몇少ない应用场景 중 하나):

function assertNever(x: never) {
  throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
  switch (s.kind) {
    case "square": return s.size * s.size;
    // case "circle": return s.radius * s.radius;
    // 오류 Argument of type 'Circle' is not assignable to parameter of type 'never'.
    default: return assertNever(s);
  }
}

완전히 커버하지 않은 경우, default 분기에 진행하여 s: Shapex: never 에 전달하여 타입 오류를 발생시킵니다 (완전히 커버한 경우, default 는 도달 불가능 분기로, never 오류를 발생시키지 않습니다). 완전성 커버 요구를 만족시킬 수 있지만, 추가로 assertNever 함수를 정의해야 합니다

P.S. Never 타입에 대한 상세 정보는, [기본 타입_TypeScript 노트 2](/articles/기본 타입-typescript 노트 2/#articleHeader3) 참조

此外,还有一种不那么准确,但也有助于检查完整性的方法:开启 --strictNullChecks 选项,并标明函数返回值。利用默认返回 undefined 来保证完整性,例如:

// 오류 Function lacks ending return statement and return type does not include 'undefined'.
function area(s: Shape): number {
  switch (s.kind) {
    case "square": return s.size * s.size;
  }
}

실질적으로는 비공 반환값 검출로, assertNever 처럼 switch 입자에 정확하지 않으며, 비교적 취약합니다 (디폴트 반환값이 있거나, 여러 개의 switch 가 있는 경우, 완전성 체크를 파괴)

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성