前の章では、TypeScriptのenumの仕組みについて説明しました。この章では、enumの代替手段について見ていきます。
enumは、メンバー名とメンバー値をマッピングします。その間接的な処理が必要ない場合、または望まない場合は、いわゆる*プリミティブリテラル型*のユニオンを使用できます - 値ごとに1つです。詳細に入る前に、プリミティブリテラル型について学ぶ必要があります。
簡単な復習: 型は値の集合と考えることができます。
*シングルトン型*は、要素が1つだけの型です。プリミティブリテラル型はシングルトン型です
type UndefinedLiteralType = undefined;
type NullLiteralType = null;
type BooleanLiteralType = true;
type NumericLiteralType = 123;
type BigIntLiteralType = 123n; // --target must be ES2020+
type StringLiteralType = 'abc';`UndefinedLiteralType`は、単一の要素`undefined`を持つ型です。
ここで作用する2つの言語レベルを認識することが重要です(これらのレベルには本書の前半ですでに遭遇しました)。次の変数宣言を考えてみましょう
const abc: 'abc' = 'abc';プリミティブリテラル型の2つのユースケースは次のとおりです。
文字列パラメータのオーバーロード。これにより、次のメソッド呼び出しの最初の引数によって2番目の引数の型が決まります。
elem.addEventListener('click', myEventHandler);プリミティブリテラル型のユニオンを使用して、メンバーを列挙することで型を定義できます。
type IceCreamFlavor = 'vanilla' | 'chocolate' | 'strawberry';2番目のユースケースの詳細については、読み進めてください。
enumから始めて、文字列リテラル型のユニオンに変換します。
enum NoYesEnum {
No = 'No',
Yes = 'Yes',
}
function toGerman1(value: NoYesEnum): string {
switch (value) {
case NoYesEnum.No:
return 'Nein';
case NoYesEnum.Yes:
return 'Ja';
}
}
assert.equal(toGerman1(NoYesEnum.No), 'Nein');
assert.equal(toGerman1(NoYesEnum.Yes), 'Ja');`NoYesStrings`は`NoYesEnum`のユニオン型バージョンです
type NoYesStrings = 'No' | 'Yes';
function toGerman2(value: NoYesStrings): string {
switch (value) {
case 'No':
return 'Nein';
case 'Yes':
return 'Ja';
}
}
assert.equal(toGerman2('No'), 'Nein');
assert.equal(toGerman2('Yes'), 'Ja');型`NoYesStrings`は、文字列リテラル型`'No'`と`'Yes'`のユニオンです。ユニオン型演算子`|`は、集合論的ユニオン演算子`∪`に関連しています。
次のコードは、文字列リテラル型のユニオンに対して網羅性チェックが機能することを示しています
// @ts-expect-error: Function lacks ending return statement and
// return type does not include 'undefined'. (2366)
function toGerman3(value: NoYesStrings): string {
switch (value) {
case 'Yes':
return 'Ja';
}
}`'No'`のケースを忘れてしまい、TypeScriptは関数が文字列ではない値を返す可能性があると警告しています。
網羅性をより明示的にチェックすることもできました
class UnsupportedValueError extends Error {
constructor(value: never) {
super('Unsupported value: ' + value);
}
}
function toGerman4(value: NoYesStrings): string {
switch (value) {
case 'Yes':
return 'Ja';
default:
// @ts-expect-error: Argument of type '"No"' is not
// assignable to parameter of type 'never'. (2345)
throw new UnsupportedValueError(value);
}
}これで、`value`が`'No'`の場合に`default`ケースに到達すると、TypeScriptが警告を発します。
**網羅性チェックの詳細**
このトピックの詳細については、§12.7.2.2「網羅性チェックによるケースの忘れ防止」を参照してください。
文字列リテラルユニオンの1つの欠点は、メンバーではない値がメンバーと間違われる可能性があることです
type Spanish = 'no' | 'sí';
type English = 'no' | 'yes';
const spanishWord: Spanish = 'no';
const englishWord: English = spanishWord;これは、スペイン語の`'no'`と英語の`'no'`は同じ値であるため、論理的です。実際の問題は、それらに異なるアイデンティティを与える方法がないことです。
文字列リテラル型のユニオンの代わりに、シンボルシングルトン型のユニオンを使用することもできます。今回は別のenumから始めましょう
enum LogLevel {
off = 'off',
info = 'info',
warn = 'warn',
error = 'error',
}シンボルシングルトン型のユニオンに変換すると、次のようになります
const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');
// %inferred-type: unique symbol | unique symbol |
// unique symbol | unique symbol
type LogLevel =
| typeof off
| typeof info
| typeof warn
| typeof error
;なぜここで`typeof`が必要なのでしょうか? `off`などは値であり、型方程式には現れません。型演算子`typeof`は、値を型に変換することでこの問題を解決します。
前の例の2つのバリエーションを考えてみましょう。
シンボルをインライン化できますか(個別の`const`宣言を参照する代わりに)?残念ながら、型演算子`typeof`のオペランドは、識別子またはドットで区切られた識別子の「パス」でなければなりません。したがって、この構文は不正です
type LogLevel = typeof Symbol('off') | ···変数を宣言するために`const`の代わりに`let`を使用できますか?(必ずしも改善ではありませんが、それでも興味深い質問です。)
`const`で宣言された変数に対してTypeScriptが推論する、より狭い型が必要なため、できません。
// %inferred-type: unique symbol
const constSymbol = Symbol('constSymbol');
// %inferred-type: symbol
let letSymbol1 = Symbol('letSymbol1');`let`を使用すると、`LogLevel`は単に`symbol`のエイリアスになります。
`const`アサーションは通常、この種の問題を解決します。しかし、この場合は機能しません
// @ts-expect-error: A 'const' assertions can only be applied to references to enum
// members, or string, number, boolean, array, or object literals. (1355)
let letSymbol2 = Symbol('letSymbol2') as const;次の関数は、`LogLevel`のメンバーを文字列に変換します
function getName(logLevel: LogLevel): string {
switch (logLevel) {
case off:
return 'off';
case info:
return 'info';
case warn:
return 'warn';
case error:
return 'error';
}
}
assert.equal(
getName(warn), 'warn');2つのアプローチはどのように比較されますか?
スペイン語の`'no'`が英語の`'no'`と混同されたこの例を思い出してください
type Spanish = 'no' | 'sí';
type English = 'no' | 'yes';
const spanishWord: Spanish = 'no';
const englishWord: English = spanishWord;シンボルを使用すると、この問題は発生しません
const spanishNo = Symbol('no');
const spanishSí = Symbol('sí');
type Spanish = typeof spanishNo | typeof spanishSí;
const englishNo = Symbol('no');
const englishYes = Symbol('yes');
type English = typeof englishNo | typeof englishYes;
const spanishWord: Spanish = spanishNo;
// @ts-expect-error: Type 'unique symbol' is not assignable to type 'English'. (2322)
const englishWord: English = spanishNo;ユニオン型とenumには、いくつかの共通点があります
しかし、それらはまた異なります。シンボルシングルトン型のユニオンの欠点は次のとおりです。
シンボルシングルトン型のユニオンの長所は次のとおりです。
判別可能なユニオンは、関数型プログラミング言語の代数データ型に関連しています。
それらがどのように機能するかを理解するために、次のような式を表すデータ構造*構文木*を考えてみましょう。
1 + 2 + 3
構文木は次のいずれかです。
次のステップ
これは、構文木の典型的なオブジェクト指向実装です
// Abstract = can’t be instantiated via `new`
abstract class SyntaxTree1 {}
class NumberValue1 extends SyntaxTree1 {
constructor(public numberValue: number) {
super();
}
}
class Addition1 extends SyntaxTree1 {
constructor(public operand1: SyntaxTree1, public operand2: SyntaxTree1) {
super();
}
}`SyntaxTree1`は、`NumberValue1`と`Addition1`のスーパークラスです。キーワード`public`は、以下の糖衣構文です。
これは、`SyntaxTree1`を使用する例です
const tree = new Addition1(
new NumberValue1(1),
new Addition1(
new NumberValue1(2),
new NumberValue1(3), // trailing comma
), // trailing comma
);注: 引数リストの末尾のカンマは、ECMAScript 2016以降のJavaScriptで許可されています。
ユニオン型(行A)を介して構文木を定義する場合、オブジェクト指向継承は必要ありません
class NumberValue2 {
constructor(public numberValue: number) {}
}
class Addition2 {
constructor(public operand1: SyntaxTree2, public operand2: SyntaxTree2) {}
}
type SyntaxTree2 = NumberValue2 | Addition2; // (A)`NumberValue2`と`Addition2`にはスーパークラスがないため、コンストラクターで`super()`を呼び出す必要はありません。
興味深いことに、以前と同じ方法でツリーを作成します
const tree = new Addition2(
new NumberValue2(1),
new Addition2(
new NumberValue2(2),
new NumberValue2(3),
),
);最後に、判別可能なユニオンについて説明します。これらは`SyntaxTree3`の型定義です
interface NumberValue3 {
kind: 'number-value';
numberValue: number;
}
interface Addition3 {
kind: 'addition';
operand1: SyntaxTree3;
operand2: SyntaxTree3;
}
type SyntaxTree3 = NumberValue3 | Addition3;クラスからインターフェースに切り替え、したがってクラスのインスタンスからプレーンオブジェクトに切り替えました。
判別可能なユニオンのインターフェースは、少なくとも1つのプロパティを共通して持ち、そのプロパティはそれぞれに対して異なる値を持つ必要があります。そのプロパティは、*判別子*または*タグ*と呼ばれます。 `SyntaxTree3`の判別子は`.kind`です。その型は文字列リテラル型です。
比較
これは、`SyntaxTree3`に一致するオブジェクトです
const tree: SyntaxTree3 = { // (A)
kind: 'addition',
operand1: {
kind: 'number-value',
numberValue: 1,
},
operand2: {
kind: 'addition',
operand1: {
kind: 'number-value',
numberValue: 2,
},
operand2: {
kind: 'number-value',
numberValue: 3,
},
}
};行Aの型注釈は必要ありませんが、データが正しい構造になっていることを確認するのに役立ちます。ここで行わないと、後で問題が発生します。
次の例では、`tree`の型は判別可能なユニオンです。判別子(行C)をチェックするたびに、TypeScriptはそれに応じて静的型を更新します
function getNumberValue(tree: SyntaxTree3) {
// %inferred-type: SyntaxTree3
tree; // (A)
// @ts-expect-error: Property 'numberValue' does not exist on type 'SyntaxTree3'.
// Property 'numberValue' does not exist on type 'Addition3'.(2339)
tree.numberValue; // (B)
if (tree.kind === 'number-value') { // (C)
// %inferred-type: NumberValue3
tree; // (D)
return tree.numberValue; // OK!
}
return null;
}行Aでは、まだ判別子`.kind`をチェックしていません。したがって、`tree`の現在の型はまだ`SyntaxTree3`であり、行Bのプロパティ`.numberValue`にアクセスできません(ユニオンの型の1つだけがこのプロパティを持っているためです)。
行Dでは、TypeScriptは`.kind`が`'number-value'`であることを認識しており、したがって`tree`の型`NumberValue3`を推測できます。そのため、次の行で`.numberValue`にアクセスすることは、今回は問題ありません。
このステップは、判別可能なユニオンの関数をどのように実装するかの例で締めくくります.
すべてのサブタイプのメンバーに適用できる操作がある場合、クラスと判別可能なユニオンのアプローチは異なります
次の例は、関数型アプローチを示しています。判別子は行Aで検査され、2つの`switch`ケースのどちらが実行されるかが決定されます。
function syntaxTreeToString(tree: SyntaxTree3): string {
switch (tree.kind) { // (A)
case 'addition':
return syntaxTreeToString(tree.operand1)
+ ' + ' + syntaxTreeToString(tree.operand2);
case 'number-value':
return String(tree.numberValue);
}
}
assert.equal(syntaxTreeToString(tree), '1 + 2 + 3');TypeScriptは、判別可能なユニオンに対して網羅性チェックを実行することに注意してください。ケースを忘れると、TypeScriptは警告を発します。
これは、前のコードのオブジェクト指向バージョンです
abstract class SyntaxTree1 {
// Abstract = enforce that all subclasses implement this method:
abstract toString(): string;
}
class NumberValue1 extends SyntaxTree1 {
constructor(public numberValue: number) {
super();
}
toString(): string {
return String(this.numberValue);
}
}
class Addition1 extends SyntaxTree1 {
constructor(public operand1: SyntaxTree1, public operand2: SyntaxTree1) {
super();
}
toString(): string {
return this.operand1.toString() + ' + ' + this.operand2.toString();
}
}
const tree = new Addition1(
new NumberValue1(1),
new Addition1(
new NumberValue1(2),
new NumberValue1(3),
),
);
assert.equal(tree.toString(), '1 + 2 + 3');それぞれのアプローチは、ある種の拡張性をうまく行います
オブジェクト指向アプローチでは、新しい操作を追加する場合、各クラスを変更する必要があります。ただし、新しい型を追加しても、既存のコードを変更する必要はありません。
関数型アプローチでは、新しい型を追加する場合、各関数を変更する必要があります。対照的に、新しい操作を追加するのは簡単です。
判別可能なユニオンと通常のユニオン型には、2つの共通点があります
次の2つの小節では、通常のユニオンよりも識別ユニオンが持つ2つの利点について説明します。
識別ユニオンを使用すると、値に記述的なプロパティ名が付けられます。比較してみましょう。
通常のユニオン
type FileGenerator = (webPath: string) => string;
type FileSource1 = string|FileGenerator;識別ユニオン
interface FileSourceFile {
type: 'FileSourceFile',
nativePath: string,
}
interface FileSourceGenerator {
type: 'FileSourceGenerator',
fileGenerator: FileGenerator,
}
type FileSource2 = FileSourceFile | FileSourceGenerator;ソースコードを読む人は、文字列がネイティブパス名であることをすぐに理解できます。
TypeScriptではユニオンの型を区別できないため、次の識別ユニオンは通常のユニオンとして実装できません。
interface TemperatureCelsius {
type: 'TemperatureCelsius',
value: number,
}
interface TemperatureFahrenheit {
type: 'TemperatureFahrenheit',
value: number,
}
type Temperature = TemperatureCelsius | TemperatureFahrenheit;列挙型を実装するための次のパターンは、JavaScriptでよく使用されます。
const Color = {
red: Symbol('red'),
green: Symbol('green'),
blue: Symbol('blue'),
};TypeScriptでは、次のように使用を試みることができます。
// %inferred-type: symbol
Color.red; // (A)
// %inferred-type: symbol
type TColor2 = // (B)
| typeof Color.red
| typeof Color.green
| typeof Color.blue
;
function toGerman(color: TColor): string {
switch (color) {
case Color.red:
return 'rot';
case Color.green:
return 'grün';
case Color.blue:
return 'blau';
default:
// No exhaustiveness check (inferred type is not `never`):
// %inferred-type: symbol
color;
// Prevent static error for return type:
throw new Error();
}
}しかし、`Color` の各プロパティの型は `symbol`(A行)であり、`TColor`(B行)は `symbol` のエイリアスです。その結果、`toGerman()` に任意のシンボルを渡すことができ、TypeScriptはコンパイル時にエラーを出しません。
assert.equal(
toGerman(Color.green), 'grün');
assert.throws(
() => toGerman(Symbol())); // no static error!`const` アサーションはこの種の問題に役立つことがよくありますが、今回は役に立ちません。
const ConstColor = {
red: Symbol('red'),
green: Symbol('green'),
blue: Symbol('blue'),
} as const;
// %inferred-type: symbol
ConstColor.red;これを修正する唯一の方法は、定数を使用することです。
const red = Symbol('red');
const green = Symbol('green');
const blue = Symbol('blue');
// %inferred-type: unique symbol
red;
// %inferred-type: unique symbol | unique symbol | unique symbol
type TColor2 = typeof red | typeof green | typeof blue;const Color = {
red: 'red',
green: 'green',
blue: 'blue',
} as const; // (A)
// %inferred-type: "red"
Color.red;
// %inferred-type: "red" | "green" | "blue"
type TColor =
| typeof Color.red
| typeof Color.green
| typeof Color.blue
;`Color` のプロパティがより一般的な型 `string` を持たないように、A行に `as const` が必要です。すると、`TColor` も `string` よりも具体的な型になります。
シンボル値のプロパティを持つオブジェクトを列挙型として使用する場合と比較して、文字列値のプロパティは次のとおりです。
長所
短所
次の例は、プレーンなJavaScriptとTypeScriptで動作する Java風の列挙型パターン を示しています。
class Color {
static red = new Color();
static green = new Color();
static blue = new Color();
}
// @ts-expect-error: Function lacks ending return statement and return type
// does not include 'undefined'. (2366)
function toGerman(color: Color): string { // (A)
switch (color) {
case Color.red:
return 'rot';
case Color.green:
return 'grün';
case Color.blue:
return 'blau';
}
}
assert.equal(toGerman(Color.blue), 'blau');残念ながら、TypeScriptは網羅性チェックを実行しないため、A行でエラーが発生します。
次の表は、TypeScriptにおける列挙型とその代替案の特性をまとめたものです。
| 一意性 | 名前空間 | 反復 | メンバーシップチェック(コンパイル時) | メンバーシップチェック(実行時) | 網羅性 | |
|---|---|---|---|---|---|---|
| 数値列挙型 | - |
✔ |
✔ |
✔ |
- |
✔ |
| 文字列列挙型 | ✔ |
✔ |
✔ |
✔ |
- |
✔ |
| 文字列ユニオン | - |
- |
- |
✔ |
- |
✔ |
| シンボルユニオン | ✔ |
- |
- |
✔ |
- |
✔ |
| 識別ユニオン | - (1) |
- |
- |
✔ |
- (2) |
✔ |
| シンボルプロパティ | ✔ |
✔ |
✔ |
- |
- |
- |
| 文字列プロパティ | - |
✔ |
✔ |
✔ |
- |
✔ |
| 列挙型パターン | ✔ |
✔ |
✔ |
✔ |
✔ |
- |
表の列のタイトル
表のセル内の脚注