メインコンテンツへ移動

組合型と型ガード_TypeScript ノート 9

無料2019-03-03#TypeScript#instanceof type guards#type assertions and type guards#类型保护与类型断言#TypeScript空类型检查#TypeScript nullable check

実際には型の組合せと型の区別

一.組合型

交差型(intersection types)

複数の型を組合せて新しい型を生成し、ソース型間に「かつ」関係が存在します。例えば:

interface ObjectConstructor {
  assign<T, U>(target: T, source: U): T & U;
}

TypeScript/lib/lib.es2015.core.d.ts から抜粋)

Object.assignsource: U 上の列挙可能なプロパティを target: T にシャローコピーできるため、戻り値の型は T & U です

交差型 A & BA でもあり B でもあるため、各ソース型のすべてのメンバーを持ちます:

interface A {
  a: string;
}
interface B {
  b: number
}

let x: A & B;
// どちらも合法
x.a;
x.b;

P.S. 名前は intersection(交差)ですが、実際には「和集合を求める」ことです

聯合型(union types)

交差型に類似し、聯合型は「または」関係を持つ複数の型から組合せられます。例えば:

interface DateConstructor {
  new (value: number | string | Date): Date;
}

TypeScript/lib/lib.es2015.core.d.ts から抜粋)

Date コンストラクタは number または string または Date 型のパラメータを受け取り、対応する型は number | string | Date です

聯合型 A | BAB のどちらかであるため、すべてのソース型の共通メンバー(「交差」)のみアクセスできます:

interface A {
  id: 'a';
  a: string;
}
interface B {
  id: 'b';
  b: number
}

let x: A | B;
x.id;
// エラー Property 'a' does not exist on type 'A | B'.
x.a;
// エラー Property 'b' does not exist on type 'A | B'.
x.b;

二.型ガード

聯合型は型で構成された列挙型に相当するため、その具体型を確定できません:

聯合型 A | BAB のどちらか

これは関数シグネチャでは問題ありませんが、関数実装では、通常は具体型を区別する必要があります。例えば:

let createDate: (value: number | string | Date) => Date;
createDate = function(value) {
  let date: Date;
  if (typeof value === 'string') {
    value = value.replace(/-/g, '/');
    // ...
  }
  else if (typeof value === 'number') {/*...*/}
  else if (value instanceof Date) {/*...*/}

  return date;
};

したがって、このようなシナリオでは、「広い」聯合型を「狭く」して具体型にする必要があります。型の観点から、上記のコードは理想的には以下のようになります:

function(value) {
  // ここでは、value は聯合型で、number か string か Date のどちらか

  if (typeof value === 'string') {
    // この分岐では、value は string
  }
  else if (typeof value === 'number') {
    // この分岐では、value は number
  }
  else if (value instanceof Date) {
    // この分岐では、value は Date
  }

  // ここでは、value は聯合型で、number か string か Date のどちらか
}

つまり、型システムに「聞いて、今このものの具体型が分かったから、小さく絞って」と伝えるメカニズムが必要です

そして、このメカニズムが*型ガード(type guard)*です

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

typeof 型ガード

typeof variable === 'type' は基本型を確定する慣用的な手法であるため、TypeScript は typeof を識別し、対応する分岐下の聯合型を自動的に狭めます:

let x: number | string;
if (typeof x === 'string') {
  // 正しい typeof 型ガード、自動的に string に狭まる
  x.toUpperCase();
}

switch 文、&& などの他の分岐構造でも同様に適用されます:

switch (typeof x) {
  case 'number':
    // 正しい typeof 型ガード
    x.toFixed();
    break;
}
// 正しい typeof 型ガード
typeof x !== 'number' && x.startsWith('xxx');

注意、最後の例は面白く、xnumberstring のどちらかで、typeof 判断から number でないと分かるため、string に狭まります

具体的には、typeof 型ガードは 2 つの形式の typeof を識別できます:

  • typeof v === "typename"

  • typeof v !== "typename"

そして typenamenumberstringboolean または symbol のみで、残りの typeof 検出結果はそれほど信頼できないため(詳細は typeof 参照)、型ガードとしては使用されません。例えば:

let x: any;
if (typeof x === 'function') {
  // any 型、typeof 型ガードは適用されない
  x;
}
if (typeof x === 'object') {
  // any 型、typeof 型ガードは適用されない
  x;
}

P.S. 関連討論は、typeof a === "object" does not type the object as Object 参照

instanceof 型ガード

typeof が基本型を検出するのと同様に、instanceof はインスタンスと「クラス」の所属関係を検出するため、これも型ガードです。例えば:

let x: Date | RegExp;
if (x instanceof RegExp) {
  // 正しい instanceof 型ガード、自動的に RegExp インスタンス型に狭まる
  x.test('');
}
else {
  // 正しい 自動的に Date インスタンス型に狭まる
  x.getTime();
}

具体的には、instanceof 右側はコンストラクタである必要があり、此時左側の型は以下に狭まります:

  • そのクラスインスタンスの型(コンストラクタ prototype 属性の型)

  • (コンストラクタにオーバーロードバージョンが存在する場合)コンストラクタの戻り型で構成された聯合型

例えば:

// Case1 そのクラスインスタンスの型
let x;
if (x instanceof Date) {
  // x は any から Date に狭まる
  x.getTime();
}

// Case2 コンストラクタの戻り型で構成された聯合型
interface DateOrRegExp { 
  new(): Date;
  new(value?: string): RegExp;
}

let A: DateOrRegExp;
let y;
if (y instanceof A) {
  // y は any から RegExp | Date に狭まる
  y;
}

P.S. instanceof 型ガードの詳細情報は、4.24 Type Guards 参照

P.S. また、[class は二重の型意味を持つ](/articles/类-typescript ノート 4/#articleHeader8)、TypeScript コード内での表現形式は以下の通り:

  • クラスの型:typeof className

  • クラスインスタンスの型:typeof className.prototype または className

例えば:

class A {
  static prop = 'prop';
  id: 'b'
}

// クラスの型
let x: typeof A;
x.prop;
// エラー id はインスタンス属性、クラス上には存在しない
x.id;

// クラスインスタンスの型
let y: typeof A.prototype;
let z: A;
// 両者の型は同等
z = y;
// エラー prop は静的属性、インスタンス上には存在しない
z.prop;
z.id;

つまり、クラスインスタンスの型はコンストラクタ prototype 属性の型と同等です。しかしこれはTypeScript のコンパイル時のみ成立し、JavaScript ランタイム概念と衝突します:

class A {}
class B extends A {}
// コンストラクタ prototype 属性は親クラスインスタンス、その型は親クラスインスタンスの型
B.prototype instanceof A === true

カスタム型ガード

typeofinstanceof 型ガードは一般的なシナリオを満たせますが、より特殊な場合はカスタム型ガードを通じて型を狭められます:

interface RequestOptions {
  url: string;
  onSuccess?: () => void;
  onFailure?: () => void;
}

// カスタム型ガード、パラメータ型 any を RequestOptions に狭める
function isValidRequestOptions(opts: any): opts is RequestOptions {
  return opts && opts.url;
}

let opts;
if (isValidRequestOptions(opts)) {
  // opts は any から RequestOptions に狭まる
  opts.url;
}

カスタム型ガードは通常の関数宣言と類似していますが、戻り型部分は*型述語(type predicate)*です:

parameterName is Type

その中で parameterName は現在の関数シグネチャ中のパラメータ名である必要があります。例えば上記の opts is RequestOptions

型述語を持つ関数を呼び出した後、渡されたパラメータの型は指定型に狭められ、前 2 つの型ガードの動作と一致します:

let isNumber: (value: any) => value is number;

let x: string | number;
if (isNumber(x)) {
  // number に狭まる
  x.toFixed(2);
}
else {
  // number でなければ string
  x.toUpperCase();
}

三.Nullable と聯合型

TypeScript には空型(Void)が 2 つあります:Undefined と Null で、(Never 以外)他のすべての型のサブタイプです。したがって nullundefined は他の任意の型に代入できます:

let x: string;
x = null;
x = undefined;
// ランタイムエラー、コンパイル時はエラーにならない
x.toUpperCase();

型から見ると、Nullable 型は元の型と null | undefined で構成された聯合型に相当します(上記の例では、let x: string | null | undefined; に相当)

これは型チェックがそれほど信頼できないことを意味し、undefined/null.xxx のようなエラーを依然として避けられないためです

strictNullChecks

空型の潜在的な問題に対し、TypeScript は --strictNullChecks オプションを提供し、オンにすると空型を厳格にチェックします:

let x: string;
// エラー Type 'null' is not assignable to type 'string'.
x = null;
// エラー Type 'undefined' is not assignable to type 'string'.
x = undefined;

空になり得る型については、明示的に宣言する必要があります:

let y: string | undefined;
y = undefined;
// Type 'null' is not assignable to type 'string | undefined'.
y = null;

同時に、オプションパラメータとオプションプロパティは自動的に | undefined が付きます。例えば:

function createDate(value?: string) {
  // エラー Object is possibly 'undefined'.
  value.toUpperCase();
}

interface Animal {
  color: string;
  name?: string;
}
let x: Animal;
// エラー Type 'undefined' is not assignable to type 'string'.
x.color = undefined;
// エラー Object is possibly 'undefined'.
x.name.toUpperCase();

類似の空値関連問題はすべて露呈するため、この観点から、空型の厳格チェックは一種のコンパイル時チェックで空値を追跡する能力に相当します

! 接尾辞型アサーション

Nullable 型は実質的に聯合型であるため、同様に型の狭め問題に直面します。これに対し、TypeScript は直感的な型ガードを提供します:

function createDate(value: string | undefined) {
  // string に狭まる
  value = value || 'today';
  value.toUpperCase();
}

自動型ガードが処理できないシナリオでは、简单地に ! 接尾辞を通じて | undefined | null 成分を除去できます:

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    // ! を通じて型中の null 成分を除去し、string に狭める
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

identifier! は [型アサーション](/articles/基本型-typescript ノート 2/#articleHeader4) に相当します(型ガードとは異なります):

let x: string | undefined | null;
x!.toUpperCase();
// に相当
(<string>x).toUpperCase();
// または
(x as string).toUpperCase();
// Object is possibly 'null' or 'undefined'.
x.toUpperCase();

P.S. 型アサーションと型ガードの違いは、アサーションは一回限り(または一時的)で、型ガードは一定のスコープ下で有効であることです

参考資料

コメント

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

コメントを書く