メインコンテンツへ移動

インデックスタイプ、マップドタイプと条件付きタイプ_TypeScript ノート 12

無料2019-03-23#TypeScript#TypeScript type mapping#TypeScript type query#TypeScript conditional type#TypeScript Distributive conditional types#可分配条件类型

タイプ組合せ以外に、新しいタイプを生成するもう 2 つの方法はタイプクエリとタイプマッピングです

一.インデックスタイプ(Index types)

インデックスタイプにより、静的チェックがタイプが不確定(列挙できない)な「動的」シナリオをカバーできます。例えば:

function pluck(o, names) {
  return names.map(n => o[n]);
}

pluck 関数は o から names が指定した部��のプロパティを取り出せます。2 つのタイプ制約があります:

  • パラメータ names 中には o にあるプロパティのみ出現可能

  • 戻りタイプはパラメータ o のプロパティ値のタイプに依存

これら 2 つの制約はすべてジェネリックを通じて記述できます:

interface pluck {
  <T, K extends keyof T>(o: T, names: K[]): T[K][]
}

let obj = { a: 1, b: '2', c: false };
// パラメータ検査
// 错误 Type 'string' is not assignable to type '"a" | "b" | "c"'.
pluck(obj, ['n']);
// 戻りタイプ推論
let xs: (string | number)[] = pluck(obj, ['a', 'b']);

P.S.interface は関数タイプを記述可能、詳細は 二.関数 参照

2 つの新しいものが現れました:

  • keyof:インデックスタイプクエリ演算子(index type query operator)

  • T[K]:インデックスアクセス演算子(indexed access operator):

インデックスタイプクエリ演算子

keyof T はタイプ T 上のすべての public プロパティ名を取得してユニオンタイプを構成します。例えば:

// 等价于 let t: { a: number; b: string; c: boolean; }
let t: typeof obj;
// 等价于 let availableKeys: "a" | "b" | "c"
let availableKeys: keyof typeof obj;

declare class Person {
  private married: boolean;
  public name: string;
  public age: number;
}
// 等价于 let publicKeys: "name" | "age"
let publicKeys: keyof Person;

P.S. 注意、typeof が値面向であるのとは異なり、keyof はタイプ面向 です。値ではありません(そのため keyof obj は不合法)

このタイプクエリ能力は pluck など事前に得知できない(または列挙できない)プロパティ名のシナリオで非常に意義があります

インデックスアクセス演算子

keyof と類似し、もう 1 つのタイプクエリ能力はインデックスでタイプにアクセスすること(T[K])で、タイプレベルのプロパティアクセス演算子 に相当します:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
  return o[name]; // o[name] is of type T[K]
}

let c: boolean = getProperty(obj, 'c');
// 等价于
let cValue: typeof obj['c'] = obj['c'];

つまり、もし t: Tk: K なら、t[k]: T[K] です:

type typesof<T, K extends keyof T> = T[K];

let a: typesof<typeof obj, 'a'> = obj['a'];
let bOrC: typesof<typeof obj, 'b' | 'c'> = obj['b'];
bOrC = obj['c'];
// 错误 Type 'number' is not assignable to type 'string | boolean'.
bOrC = obj['a'];

インデックスタイプと文字列インデックス署名

keyofT[K] は文字列 インデックス署名(index signature) にも同様に適用されます。例えば:

interface NetCache {
  [propName: string]: object;
}

// string | number タイプ
let keyType: keyof NetCache;
// object タイプ
let cached: typesof<NetCache, 'http://example.com'>;

keyType のタイプは string | number であることに気づきます。予想された string ではなく、これは JavaScript での数値インデックスは文字列インデックスに変換されるためです。例えば:

let netCache: NetCache;
netCache[20190101] === netCache['20190101']

つまり、key のタイプは文字列でも数値でもよく、つまり string | number です。もし強いて number を剔除したいなら、内蔵の Extract タイプエイリアスを通じて完成できます:

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

TypeScript/lib/lib.es5.d.ts から抜粋)

let stringKey: Extract<keyof NetCache, string> = 'http://example.com';

もちろん、一般にこのようにする必要はありません。タイプ角度から見ると、key: string | number は合理的だからです

P.S. より多くの関連討論は、Keyof inferring string | number when key is only a string 参照

二.マップドタイプ

インデックスタイプと類似し、既存タイプから新しいタイプを派生させるもう 1 つの方法はマッピングを行うことです:

In a mapped type, the new type transforms each property in the old type in the same way.

例えば:

type Stringify<T> = {
  [P in keyof T]: string
}

// すべてのプロパティ値を toString() 一遍
function toString<T>(obj: T): Stringify<T> {
  return Object.keys(obj)
    .reduce((a, k) =>
      ({ ...a, [k]: obj[k].toString() }),
      Object.create(null)
    );
}

let stringified = toString({ a: 1, b: 2 });
// 错误 Type 'number' is not assignable to type 'string'.
stringified = { a: 1 };

Stringify{ [propName: string]: any } から { [propName: string]: string } へのタイプマッピングを実装しましたが、それほど有用には見えません。実際、より一般的な用法はマッピングタイプを通じて key の属性を変更することです。例えばあるタイプのすべてのプロパティをオプションまたは読み取り専用にすること:

type Partial<T> = {
  [P in keyof T]?: T[P];
}
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
}

TypeScript/lib/lib.es5.d.ts から抜粋)

let obj = { a: 1, b: '2' };
let constObj: Readonly<typeof obj>;
let optionalObj: Partial<typeof obj>;

// 错误 Cannot assign to 'a' because it is a read-only property.
constObj.a = 2;
// 错误 Type '{}' is missing the following properties from type '{ a: number; b: string; }': a, b
obj = {};
// 正确
optionalObj = {};

構文フォーマット

最も直感的な例:

// 1 つの「タイプ集」を探す
type Keys = 'a' | 'b';
// タイプマッピングを通じて新しいタイプ { a: boolean, b: boolean } を得る
type Flags = { [K in Keys]: boolean };

[K in Keys] 形式上はインデックス署名と類似していますが、for...in 構文を融合しています。その中で:

  • K:タイプ変数、順に各プロパティにバインドされ、各プロパティ名のタイプに対応

  • Keys:文字列リテラルで構成される [ユニオンタイプ](/articles/組合せタイプとタイプ保護-typescript ノート 9/#articleHeader3)、一組のプロパティ名(のタイプ)を表す

  • boolean:マッピング結果タイプ、つまり各プロパティ値のタイプ

類似して、[P in keyof T]keyof T を(プロパティ名)タイプ集として探し、从而に既存タイプをマッピングして新しいタイプを得ます

P.S.另外、PartialReadonly はすべて源タイプ情報を完全に保留できます(入力された源タイプからプロパティ名及び値タイプを取得し、修飾子上の差異のみ存在し、源タイプと新しいタイプ間��互換関係があります)。同態(homomorphic) 変換と呼ばれ、Stringify は源プロパティ値タイプを丢弃し、非同態(non-homomorphic)変換に属します

「開箱」推論(unwrapping inference)

タイプをマッピングすることはタイプレベルの「装箱」 に相当します:

// 包装タイプ
type Proxy<T> = {
  get(): T;
  set(value: T): void;
}
// 装箱(普通タイプ to 包装タイプのタイプマッピング)
type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>;
}
// 装箱関数
function proxify<T>(o: T): Proxify<T> {
  let result: Proxify<T>;
  // ... wrap proxies ...
  return result;
}

例えば:

// 普通タイプ
interface Person { 
    name: string,
    age: number
}
let lily: Person;
// 装箱
let proxyProps: Proxify<Person> = proxify(lily);

同様に、「開箱」もできます:

function unproxify<T>(t: Proxify<T>): T {
  let result = {} as T;
  for (const k in t) {
    result[k] = t[k].get();
  }
  return result;
}

let originalProps: Person = unproxify(proxyProps);

最終行の unproxify 関数タイプを自動的に推論できます:

function unproxify<Person>(t: Proxify<Person>): Person

パラメータタイプ proxyProps: Proxify<Person> から Person を取り出して戻り値タイプとし、いわゆる「開箱」です

三.条件付きタイプ

条件付きタイプは非均一タイプマッピング(non-uniform type mapping)を表現するために使用され、タイプ互換関係(つまり条件)に基づいて 2 つのタイプから 1 つを選択できます:

T extends U ? X : Y

意味は三項演算子に類似し、もし TU のサブタイプなら X タイプ、否则 Y タイプです。另外、もう 1 つの状況は条件の真偽が確定できない(TU のサブタイプかどうか確定できない)場合で、此時 X | Y タイプです。例えば:

declare function f<T extends boolean>(x: T): T extends true ? string : number;

// x のタイプ为 string | number
let x = f(Math.random() < 0.5)

另外、もし T または U がタイプ変数を含むなら、タイプ変数がすべて対応する具体的なタイプを持ってから条件付きタイプの結果を得られます:

When T or U contains type variables, whether to resolve to X or Y, or to defer, is determined by whether or not a the type system has enough information to conclude that T is always assignable to U.

例えば:

interface Foo {
  propA: boolean;
  propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;

function foo<U>(x: U) {
  // a のタイプ为 U extends Foo ? string : number
  let a = f(x);
  let b: string | number = a;
}

その中で a のタイプは U extends Foo ? string : number(つまり条件が不確定な状況)で、f(x)x のタイプ U がまだ確定せず、UFoo のサブタイプかどうか得知できません。しかし条件付きタイプ无非 2 つの可能タイプなので、let b: string | number = a; は必ず合法です(x が何のタイプであっても)

分配可能条件付きタイプ

分配可能条件付きタイプ(distributive conditional type)中被検査のタイプは裸タイプパラメータ(naked type parameter)です。その特殊な点は分配法則を満たすことです:

(A | B | C) extends U ? X : Y
等价于
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

例えば:

// 嵌套の条件付きタイプはパターンマッチングに類似
type TypeName<T> =
  T extends string ? "string" :
    T extends number ? "number" :
      T extends boolean ? "boolean" :
        T extends undefined ? "undefined" :
          T extends Function ? "function" : "object";

// T タイプ等价于ユニオンタイプ string" | "function
type T = TypeName<string | (() => void)>;

另外、T extends U ? X : Y 中、X 中に現れる T はすべて U タイプ制約を持ちます:

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

// T タイプ等价于ユニオンタイプ BoxedValue<string> | BoxedArray<boolean>
type T = Boxed<string | boolean[]>;

上例中 Boxed<T> の True 分枝は any[] タイプ制約を持つため、インデックスアクセス(T[number])を通じて配列要素のタイプを得られます

応用シナリオ

条件付きタイプはマッピングタイプと結合して针对性的なタイプマッピングを実現できます(異なる源タイプは異なるマッピングルールに対応可能)。例えば:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
// すべての関数タイプのプロパティを摘出
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

interface Part {
  id: number;
  name: string;
  subparts: Part[];
  updatePart(newName: string): void;
}
// T タイプ等价于文字列リテラルタイプ "updatePart"
type T = FunctionPropertyNames<Part>;

而分配可能条件付きタイプは通常ユニオンタイプを篩選するために使用されます:

type Diff<T, U> = T extends U ? never : T;

// T タイプ等价于ユニオンタイプ "b" | "d"
type T = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;

// より一歩進んだ
type NeverNullable<T> = Diff<T, null | undefined>;
function f1<T>(x: T, y: NeverNullable<T>) {
  x = y;
  // 错误 Type 'T' is not assignable to type 'Diff<T, null>'.
  y = x;
}

条件付きタイプ中のタイプ推論

条件付きタイプの extends 子句中、infer 声明を通じて推論されるタイプ変数を導入できます。例えば:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

上例中タイプ変数 R を導入して関数戻りタイプを表し、True 分枝中で引用し、从而に戻りタイプを抽出します

P.S. 特殊的、もしオーバーロードが存在するなら、最後の署名を取ります(慣例に従い、最後は通常最も広範)して推論します。例えば:

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;

// T タイプ等价于ユニオンタイプ string | number
type T = ReturnType<typeof foo>;

P.S. より多くの例は Type inference in conditional types 参照

事前定義の条件付きタイプ

TypeScript はさらにいくつかの常用的な条件付きタイプを内蔵しています:

// T から U に属するサブタイプの部分を去掉、つまり之前の例中の Diff
type Exclude<T, U> = T extends U ? never : T;
// T から U に属するサブタイプの部分を篩選、之前の例中の Filter
type Extract<T, U> = T extends U ? T : never;
// T から null と undefined 部分を去掉
type NonNullable<T> = T extends null | undefined ? never : T;
// 関数タイプの戻りタイプを取り出す
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// 构造函数タイプの示例タイプを取り出す
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

TypeScript/lib/lib.es5.d.ts から抜粋)

具体例は Predefined conditional types 参照

四.まとめ

[タイプ組合せ](/articles/組合せタイプとタイプ保護-typescript ノート 9/#articleHeader1) 以外に、新しいタイプを生成するもう 2 つの方法はタイプクエリとタイプマッピングです

タイプクエリ:

  • インデックスタイプ:既存タイプの一部を取り出して新しいタイプを生成

タイプマッピング:

  • マップドタイプ:既存タイプをマッピングして新しいタイプを得る
  • 条件付きタイプ:タイプ互換関係を条件として簡単な三項演算を行うことを許可し、非均一タイプマッピングを表現するために使用

参考資料

コメント

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

コメントを書く