メインコンテンツへ移動

タイプエイリアスとリテラルタイプ_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 がある場合、完全性チェックを破壊)

参考資料

コメント

コメントはまだありません

コメントを書く