ウェブサイト検索

TypeScript - 実行時に文字列リテラルのユニオン型の有効性を確認するには?


私は文字列リテラルの単純なユニオン型を持っており、通常のJavaScriptへのFFI呼び出しのためにその有効性を確認する必要があります。特定の変数がランタイムでこれらのリテラル文字列のいずれかのインスタンスであることを保証する方法はありますか?例えば、次のような感じです

type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false

回答 #1

Typescript 3.8.3 の時点では、この件に関する明確なベストプラクティスはありません。外部ライブラリに依存しない解決策は3つあるようです。いずれの場合も、文字列をランタイムで利用可能なオブジェクト(例:配列)に格納する必要があります。

これらの例では、文字列が既知の標準的な羊の名前のいずれかであるかをランタイムで確認する関数が必要であると仮定します。私たちが知っている名前は Capn Frisky、Mr. Snugs、Lambchop です。TypeScript コンパイラが理解できる方法でこれを行う3つの方法を紹介します。

1: 型アサーション(より簡単)

ヘルメットを外し、自分でタイプを確認し、アサーションを使用してください。

const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number]; // "Capn Frisky" | "Mr. Snugs" | "Lambchop"

// This string will be read at runtime: the TS compiler can't know if it's a SheepName.
const unsafeJson = '"Capn Frisky"';

/**
 * Return a valid SheepName from a JSON-encoded string or throw.
 */
function parseSheepName(jsonString: string): SheepName {
    const maybeSheepName: unknown = JSON.parse(jsonString);
    // This if statement verifies that `maybeSheepName` is in `sheepNames` so
    // we can feel good about using a type assertion below.
    if (typeof maybeSheepName === 'string' && sheepNames.includes(maybeSheepName)) {
        return (maybeSheepName as SheepName); // type assertion satisfies compiler
    }
    throw new Error('That is not a sheep name.');
}

const definitelySheepName = parseSheepName(unsafeJson);

利点: シンプルで、理解しやすい。

短所: 壊れやすい。TypeScript はあなたが maybeSheepName を適切に検証したと信じているだけです。もし検証を誤って削除してしまうと、TypeScript は自分自身を守ってくれません。

2: カスタム型ガード(より再利用可能)

これは上記の型アサーションのより洗練された、より一般的なバージョンですが、それでも単なる型アサーションです。

const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];

const unsafeJson = '"Capn Frisky"';

/**
 * Define a custom type guard to assert whether an unknown object is a SheepName.
 */
function isSheepName(maybeSheepName: unknown): maybeSheepName is SheepName {
    return typeof maybeSheepName === 'string' && sheepNames.includes(maybeSheepName);
}

/**
 * Return a valid SheepName from a JSON-encoded string or throw.
 */
function parseSheepName(jsonString: string): SheepName {
    const maybeSheepName: unknown = JSON.parse(jsonString);
    if (isSheepName(maybeSheepName)) {
        // Our custom type guard asserts that this is a SheepName so TS is happy.
        return (maybeSheepName as SheepName);
    }
    throw new Error('That is not a sheep name.');
}

const definitelySheepName = parseSheepName(unsafeJson);

賛: より再利用可能で、わずかに壊れにくく、おそらくより読みやすい。

短所:TypeScript はまだあなたの言うことを信じているだけです。こんなに単純なことにしてはコードが多すぎるようです。

3: Array.find を使用する(最も安全、推奨)

これは型アサーションを必要としません。もしあなたが私のように自分を信用していない場合に備えて。

const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];

const unsafeJson = '"Capn Frisky"';

/**
 * Return a valid SheepName from a JSON-encoded string or throw.
 */
function parseSheepName(jsonString: string): SheepName {
    const maybeSheepName: unknown = JSON.parse(jsonString);
    const sheepName = sheepNames.find((validName) => validName === maybeSheepName);
    if (sheepName) {
        // `sheepName` comes from the list of `sheepNames` so the compiler is happy.
        return sheepName;
    }
    throw new Error('That is not a sheep name.');
}

const definitelySheepName = parseSheepName(unsafeJson);

利点: 型アサーションを必要とせず、コンパイラーがすべての検証を行っています。それが私には重要なので、この解決策を好みます。

デメリット:ちょっと変に見えます。パフォーマンスの最適化が難しくなります。


というわけです。これらの戦略のいずれかを合理的に選ぶことも、他の人が推奨するサードパーティのライブラリを使うこともできます。

厳密な人は、ここで配列を使うのは非効率的だと正しく指摘するでしょう。sheepNames 配列をセットにキャストして O(1) のルックアップを行うことで、これらの解決策を最適化できます。もし何千もの潜在的な羊の名前(またはその他のもの)を扱う場合には、それだけの価値があります。

回答 #2

TypeScript 2.1以降では、keyof 演算子を使って逆のことができます。

アイデアは次の通りです。文字列リテラル型の情報はランタイムでは利用できないため、文字列リテラルをキーとする通常のオブジェクトを定義し、そのオブジェクトのキーの型を作成します。

以下の通り:

// Values of this dictionary are irrelevant
const myStrings = {
  A: "",
  B: ""
}

type MyStrings = keyof typeof myStrings;

isMyStrings(x: string): x is MyStrings {
  return myStrings.hasOwnProperty(x);
}

const a: string = "A";
if(isMyStrings(a)){
  // ... Use a as if it were typed MyString from assignment within this block: the TypeScript compiler trusts our duck typing!
}

回答 #3

プログラムでいくつかの文字列ユニオン定義があり、それらをランタイムでチェックできるようにしたい場合、ジェネリックなStringUnion関数を使用して、それらの静的型と型チェック用メソッドを一緒に生成することができます。

汎用サポート機能

// TypeScript will infer a string union type from the literal values passed to
// this function. Without `extends string`, it would instead generalize them
// to the common string type. 
export const StringUnion = <UnionType extends string>(...values: UnionType[]) => {
  Object.freeze(values);
  const valueSet: Set<string> = new Set(values);

  const guard = (value: string): value is UnionType => {
    return valueSet.has(value);
  };

  const check = (value: string): UnionType => {
    if (!guard(value)) {
      const actual = JSON.stringify(value);
      const expected = values.map(s => JSON.stringify(s)).join(' | ');
      throw new TypeError(`Value '${actual}' is not assignable to type '${expected}'.`);
    }
    return value;
  };

  const unionNamespace = {guard, check, values};
  return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType});
};

例の定義

また、生成された型を抽出し、その定義を名前空間オブジェクトとマージするための定型文も必要です。この定義がエクスポートされ、別のモジュールにインポートされると、マージされた定義が自動的に取得されます。利用者は自分で型を再抽出する必要はありません。

const Race = StringUnion(
  "orc",
  "human",
  "night elf",
  "undead",
);
type Race = typeof Race.type;

使用例

コンパイル時に、Race 型は、"orc" | "human" | "night elf" | "undead" のように文字列のユニオンを通常どおり定義した場合と同じように動作します。また、値がユニオンのメンバーであるかどうかを返し、型ガードとして使用できる .guard(...) 関数と、有効な場合はその値を返し、そうでなければ TypeError をスローする .check(...) 関数も用意されています。

let r: Race;
const zerg = "zerg";

// Compile-time error:
// error TS2322: Type '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = zerg;

// Run-time error:
// TypeError: Value '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = Race.check(zerg);

// Not executed:
if (Race.guard(zerg)) {
  r = zerg;
}

より一般的な解決策:runtypes

このアプローチは runtypes ライブラリに基づいており、TypeScript でほぼすべての型を定義し、実行時の型チェッカーを自動的に取得するための類似の関数を提供します。この特定のケースでは少し冗長になるかもしれませんが、より柔軟なものが必要な場合は試してみることを検討してください。

例の定義

import {Union, Literal, Static} from 'runtypes';

const Race = Union(
  Literal('orc'),
  Literal('human'),
  Literal('night elf'),
  Literal('undead'),
);
type Race = Static<typeof Race>;

使用例は上記と同じです。

回答 #4

あなたはenumを使うことができ、その後に文字列がEnumにあるかどうかをチェックできます

export enum Decisions {
    approve = 'approve',
    reject = 'reject'
}

export type DecisionsTypeUnion =
    Decisions.approve |
    Decisions.reject;

if (decision in Decisions) {
  // valid
}

関連記事