Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScript タイプシステム

構造化型システム#

class Cat {
  meow() { }
  eat() { } // eatを削除してもエラーは出ない
}

class Dog {
  eat() { }
}

function feedCat(cat: Cat) { }

// エラー!
feedCat(new Dog())

これは、TypeScript が 2 つの型を名前で比較するのではなく(つまり、feedCat関数は Cat 型でのみ呼び出せる)、実際にそれぞれの型が持つ属性とメソッドを比較するからです。つまり、ここでは実際に Cat 型の属性が Dog 型に存在するかどうかを比較しています。

最初の例では、Cat と Dog 型のメソッドは一致しているため、異なる名前の 2 つの型であっても、構造が一致していると見なされます。これが構造化型システムの特性です。構造型の別名ダックタイプ(Duck Typing)を聞いたことがあるかもしれません。この名前はダックテスト(Duck Test)に由来しています。その核心理念は、もしあなたが見た鳥がダックのように歩き、ダックのように泳ぎ、ダックのように鳴くなら、その鳥はダックです

class Cat {
  eat() { }
}

class Dog {
  bark() { }
  eat() { }
}

function feedCat(cat: Cat) { }

feedCat(new Dog())

この時、なぜ型エラーが発生しないのでしょうか?これは、構造化型システムが Dog 型が完全に Cat 型を実装していると見なすからです。追加のメソッドbarkについては、Dog 型が Cat 型を継承した後に追加された新しいメソッドと見なすことができ、したがって Dog クラスは Cat クラスのサブクラスと見なすことができます。同様に、オブジェクト指向プログラミングのリスコフの置換原則もダックテストに言及しています:もしそれがダックのように見え、ダックのように鳴くが、動作するために電池が必要なら、あなたの抽象はおそらく間違っている

さらに、オブジェクト型の属性を比較する際にも、同様に構造化型システムが判断に使用されます。また、構造内の関数型(つまりメソッド)を比較する際にも、型の互換性比較が存在します:

class Cat {
  eat(): boolean {
    return true
  }
}

class Dog {
  eat(): number {
    return 599;
  }
}

function feedCat(cat: Cat) { }

// エラー!
feedCat(new Dog())

名称型システム#

名称型システム(Nominal Typing System)は、2 つの互換性のある型がその名前が完全に一致する必要があることを要求します。例えば、以下のコード:

type USD = number;
type CNY = number;

const CNYCount: CNY = 200;
const USDCount: USD = 200;

function addCNY(source: CNY, input: CNY) {
  return source + input;
}

addCNY(CNYCount, USDCount)

名称型システムでは、CNY と USD は完全に異なる 2 つの型と見なされるため、この状況を回避できます。『プログラミングと型システム』という本では、型の重要な意義の 1 つはデータの利用可能な操作と実際の意味を制限することであり、これは名称型システムにおいてより明確に表れます。例えば、上記のように型の構造を通じて、構造化型システムが 2 つの型が親子型関係を持つと見なすことができる一方で、名称型システムでは親子型関係は明示的な継承を通じてのみ実現でき、これを ** 名義的サブタイプ(Nominal Subtyping)** と呼びます。

TypeScript で名称型システムを模倣する#

export declare class TagProtector<T extends string> {
  protected __tag__: T;
}

export type Nominal<T, U extends string> = T & TagProtector<U>;

ここでは、TagProtector を使用してprotected属性を持つクラスを宣言し、それを追加情報を持たせるために使用し、元の型と統合することで Nominal ツール型を得ています。

export type CNY = Nominal<number, 'CNY'>;

export type USD = Nominal<number, 'USD'>;

const CNYCount = 100 as CNY;

const USDCount = 100 as USD;

function addCNY(source: CNY, input: CNY) {
  return (source + input) as CNY;
}

addCNY(CNYCount, CNYCount);

// エラーが発生しました!
addCNY(CNYCount, USDCount);

型階層#

image

型互換性を判断する方法#

変数 a = 変数 b の場合、成立することは、<変数bの型> extends <変数aの型>が成立することを意味し、つまりb 型は a 型のサブタイプです

type Result = 'linbudu' extends string ? 1 : 2;
declare let source: string;

declare let anyType: any;
declare let neverType: never;

anyType = source;

// 型“string”を型“never”に割り当てることはできません。
neverType = source;

基本型とリテラル型#

type Result1 = "linbudu" extends string ? 1 : 2; // 1
type Result2 = 1 extends number ? 1 : 2; // 1
type Result3 = true extends boolean ? 1 : 2; // 1
type Result4 = { name: string } extends object ? 1 : 2; // 1
type Result5 = { name: 'linbudu' } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1

明らかに、基本型とそれに対応するリテラル型は必ず親子型関係に存在します。厳密に言えば、object がここに現れるのは適切ではなく、実際にはすべての非原始型の型、つまり配列、オブジェクト、関数型を表します。したがって、ここで Result6 が成立する理由は、[]というリテラル型も object のリテラル型と見なすことができるからです。結論を簡潔にまとめると、リテラル型 < 対応する原始型です。

ユニオン型#

type Result7 = 1 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result8 = 'lin' extends 'lin' | 'bu' | 'du' ? 1 : 2; // 1
type Result9 = true extends true | false ? 1 : 2; // 1
type Result10 = string extends string | false | number ? 1 : 2; // 1

リテラル型 < このリテラル型を含むユニオン型、原始型 < この原始型を含むユニオン型

type Result11 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1
type Result12 = {} | (() => void) | [] extends object ? 1 : 2; // 1

同一基本型のリテラルユニオン型 < この基本型

結論をまとめると、特別な場合を除いて、最終的な結論は次のようになります:リテラル型 < このリテラル型を含むユニオン型(同一基本型) < 対応する原始型、すなわち:

// 2
type Result13 = 'linbudu' extends 'linbudu' | '599'
  ? 'linbudu' | '599' extends string
    ? 2
    : 1
  : 0;

ボックス型#

type Result14 = string extends String ? 1 : 2; // 1
type Result15 = String extends {} ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends Object ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends {} ? 1 : 2; // 1

type Result17 = object extends Object ? 1 : 2; // 1
type Result20 = Object extends object ? 1 : 2; // 1

type Result19 = Object extends {} ? 1 : 2; // 1
type Result21 = {} extends Object ? 1 : 2; // 1

原始型 < 原始型に対応するボックス型 < Object 型

トップ型#

type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1
type Result24 = any extends Object ? 1 : 2; // 1 | 2
type Result25 = unknown extends Object ? 1 : 2; // 2
type Result26 = any extends 'linbudu' ? 1 : 2; // 1 | 2
type Result27 = any extends string ? 1 : 2; // 1 | 2
type Result28 = any extends {} ? 1 : 2; // 1 | 2
type Result29 = any extends never ? 1 : 2; // 1 | 2

TypeScript 内部のコードの条件型処理において、もし判断されるのが any であれば、条件型の結果から構成されるユニオン型を直接返します

type Result31 = any extends unknown ? 1 : 2;  // 1
type Result32 = unknown extends any ? 1 : 2;  // 1

Object < any / unknown

ボトム型#

type Result33 = never extends 'linbudu' ? 1 : 2; // 1
type Result34 = undefined extends 'linbudu' ? 1 : 2; // 2
type Result35 = null extends 'linbudu' ? 1 : 2; // 2
type Result36 = void extends 'linbudu' ? 1 : 2; // 2

never < リテラル型

その他の比較シーン#

  • 基本クラスと派生クラスについて、通常、派生クラスは基本クラスの構造を完全に保持します。構造化型の比較において、その型は自然にサブタイプ関係に存在します。派生クラス自体が extends 基本クラスから得られることは言うまでもありません。
  • ユニオン型の型階層比較については、** あるユニオン型が別のユニオン型のサブセットと見なされるかどうかを比較するだけで、このユニオン型のすべてのメンバーが別のユニオン型の中に見つけられる必要があります
type Result36 = 1 | 2 | 3 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result37 = 2 | 4 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result38 = 1 | 2 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
type Result39 = 1 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
  • 配列とタプル
type Result40 = [number, number] extends number[] ? 1 : 2; // 1
type Result41 = [number, string] extends number[] ? 1 : 2; // 2
type Result42 = [number, string] extends (number | string)[] ? 1 : 2; // 1
type Result43 = [] extends number[] ? 1 : 2; // 1
type Result44 = [] extends unknown[] ? 1 : 2; // 1
type Result45 = number[] extends (number | string)[] ? 1 : 2; // 1
type Result46 = any[] extends number[] ? 1 : 2; // 1
type Result47 = unknown[] extends number[] ? 1 : 2; // 2
type Result48 = never[] extends number[] ? 1 : 2; // 1

References:
https://juejin.cn/book/7086408430491172901

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。