if とタイプガードによる絞り込みswitch とタイプガードによる絞り込みunknown 型===)typeof、instanceof、Array.isArrayin を使用した個別のプロパティのチェックasserts «cond»asserts «arg» is «type»@hqoss/guards: タイプガードを持つライブラリTypeScriptでは、値の型が一部の操作に対して汎用すぎる場合があります。たとえば、ユニオン型です。この章では、次の質問に答えます。
T を T のサブセットに変更することを意味します。たとえば、型 null|string を型 string に絞り込むと便利なことがよくあります。typeof と instanceof はタイプガードです。静的型がどのように汎用すぎるかを確認するには、次の関数 getScore() を考えてみましょう。
assert.equal(
getScore('*****'), 5);
assert.equal(
getScore(3), 3);getScore() のスケルトンは次のようになります。
function getScore(value: number|string): number {
// ···
}getScore() の本体内では、value の型が number なのか string なのかわかりません。知る前に、value を実際に操作することはできません。
if とタイプガードによる絞り込み解決策は、実行時に typeof を使用して value の型を確認することです (行 A と行 B)。
function getScore(value: number|string): number {
if (typeof value === 'number') { // (A)
// %inferred-type: number
value;
return value;
}
if (typeof value === 'string') { // (B)
// %inferred-type: string
value;
return value.length;
}
throw new Error('Unsupported value: ' + value);
}この章では、型を値の集合として解釈します。(この解釈と別の解釈の詳細については、[コンテンツは含まれていません] を参照してください。)
行 A と行 B から始まる then ブロック内では、実行したチェックにより、value の静的型が変更されます。元の型 number|string のサブセットを操作しています。このように型のサイズを縮小することを *絞り込み* と呼びます。 typeof や同様の実行時操作の結果を確認することを *タイプガード* と呼びます。
絞り込みは value の元の型を変更するのではなく、チェックを通過するにつれてより具体的な型にするだけであることに注意してください。
switch とタイプガードによる絞り込みif の代わりに switch を使用した場合でも、絞り込みは機能します。
function getScore(value: number|string): number {
switch (typeof value) {
case 'number':
// %inferred-type: number
value;
return value;
case 'string':
// %inferred-type: string
value;
return value.length;
default:
throw new Error('Unsupported value: ' + value);
}
}型が汎用すぎる場合のその他の例を次に示します。
Null許容型
function func1(arg: null|string) {}
function func2(arg: undefined|string) {}判別可能なユニオン
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function func3(attendee: Attendee) {}オプションパラメータの型
function func4(arg?: string) {
// %inferred-type: string | undefined
arg;
}これらの型はすべてユニオン型であることに注意してください。
unknown 型値が unknown 型 の場合、ほとんど何もできず、最初に型を絞り込む必要があります (行 A)。
function parseStringLiteral(stringLiteral: string): string {
const result: unknown = JSON.parse(stringLiteral);
if (typeof result === 'string') { // (A)
return result;
}
throw new Error('Not a string literal: ' + stringLiteral);
}言い換えれば、unknown 型は汎用すぎるため、絞り込む必要があります。ある意味で、unknown もユニオン型です (すべての型のユニオン)。
見てきたように、*タイプガード* は、オペランドが実行時に特定の条件を満たしているかどうかによって、true または false を返す操作です。TypeScript の型推論は、結果が true の場合にオペランドの静的型を絞り込むことで、タイプガードをサポートします。
===)厳密等価はタイプガードとして機能します。
function func(value: unknown) {
if (value === 'abc') {
// %inferred-type: "abc"
value;
}
}一部のユニオン型では、=== を使用してコンポーネントを区別できます。
interface Book {
title: null | string;
isbn: string;
}
function getTitle(book: Book) {
if (book.title === null) {
// %inferred-type: null
book.title;
return '(Untitled)';
} else {
// %inferred-type: string
book.title;
return book.title;
}
}ユニオン型コンポーネントを含めるために === を使用し、除外するために !=== を使用することは、そのコンポーネントが *シングルトン型* (メンバーが 1 つだけの集合) である場合にのみ機能します。 null 型はシングルトン型です。唯一のメンバーは値 null です。
typeof、instanceof、Array.isArrayこれらは 3 つの一般的な組み込みタイプガードです。
function func(value: Function|Date|number[]) {
if (typeof value === 'function') {
// %inferred-type: Function
value;
}
if (value instanceof Date) {
// %inferred-type: Date
value;
}
if (Array.isArray(value)) {
// %inferred-type: number[]
value;
}
}then ブロック内で value の静的型がどのように絞り込まれるかに注意してください。
in を使用した個別のプロパティのチェック個別のプロパティをチェックするために使用される場合、演算子 in はタイプガードです。
type FirstOrSecond =
| {first: string}
| {second: string};
function func(firstOrSecond: FirstOrSecond) {
if ('second' in firstOrSecond) {
// %inferred-type: { second: string; }
firstOrSecond;
}
}次のチェックは機能しないことに注意してください。
function func(firstOrSecond: FirstOrSecond) {
// @ts-expect-error: Property 'second' does not exist on
// type 'FirstOrSecond'. [...]
if (firstOrSecond.second !== undefined) {
// ···
}
}この場合の問題は、絞り込みを行わずに、型が FirstOrSecond である値のプロパティ .second にアクセスできないことです。
in は非ユニオン型を絞り込みません残念ながら、in はユニオン型にのみ役立ちます。
function func(obj: object) {
if ('name' in obj) {
// %inferred-type: object
obj;
// @ts-expect-error: Property 'name' does not exist on type 'object'.
obj.name;
}
}判別可能なユニオンでは、ユニオン型のコンポーネントには、コンポーネントごとに値が異なる 1 つ以上の共通プロパティがあります。このようなプロパティは *判別子* と呼ばれます。
判別子の値のチェックはタイプガードです。
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function getId(attendee: Attendee) {
switch (attendee.kind) {
case 'Teacher':
// %inferred-type: { kind: "Teacher"; teacherId: string; }
attendee;
return attendee.teacherId;
case 'Student':
// %inferred-type: { kind: "Student"; studentId: string; }
attendee;
return attendee.studentId;
default:
throw new Error();
}
}前の例では、.kind は判別子です。ユニオン型 Attendee の各コンポーネントには、このプロパティがあり、一意の値が設定されています。
if 文と等価チェックは、switch 文と同様に機能します。
function getId(attendee: Attendee) {
if (attendee.kind === 'Teacher') {
// %inferred-type: { kind: "Teacher"; teacherId: string; }
attendee;
return attendee.teacherId;
} else if (attendee.kind === 'Student') {
// %inferred-type: { kind: "Student"; studentId: string; }
attendee;
return attendee.studentId;
} else {
throw new Error();
}
}プロパティの型 (プロパティ名のチェーンを介してアクセスするネストされたプロパティの型も含む) を絞り込むこともできます。
type MyType = {
prop?: number | string,
};
function func(arg: MyType) {
if (typeof arg.prop === 'string') {
// %inferred-type: string
arg.prop; // (A)
[].forEach((x) => {
// %inferred-type: string | number | undefined
arg.prop; // (B)
});
// %inferred-type: string
arg.prop;
arg = {};
// %inferred-type: string | number | undefined
arg.prop; // (C)
}
}前のコードのいくつかの場所を見てみましょう。
arg.prop の型を絞り込みました。.every() は絞り込みません.every() を使用してすべての配列要素が null 以外であることを確認した場合、TypeScript は mixedValues の型を絞り込みません (行 A)。
const mixedValues: ReadonlyArray<undefined|null|number> =
[1, undefined, 2, null];
if (mixedValues.every(isNotNullish)) {
// %inferred-type: readonly (number | null | undefined)[]
mixedValues; // (A)
}mixedValues は読み取り専用である必要があることに注意してください。そうでない場合、それへの別の参照によって、if 文内で null を mixedValues にプッシュすることが静的に許可されます。しかし、それは mixedValues の絞り込まれた型を正しくないものにします。
前のコードでは、次の *ユーザー定義タイプガード* を使用しています (それが何であるかについては、後ほど詳しく説明します)。
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
return value !== undefined && value !== null;
}NonNullable<Union> (行 A) は、ユーティリティ型 であり、ユニオン型 Union から undefined 型と null 型を削除します。
.filter() は、より狭い型の配列を生成します.filter() は、より狭い型の配列を生成します (つまり、既存の型を実際に絞り込むわけではありません)。
// %inferred-type: (number | null | undefined)[]
const mixedValues = [1, undefined, 2, null];
// %inferred-type: number[]
const numbers = mixedValues.filter(isNotNullish);
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
return value !== undefined && value !== null;
}残念ながら、タイプガード関数を直接使用する必要があります。タイプガードを持つアロー関数は十分ではありません。
// %inferred-type: (number | null | undefined)[]
const stillMixed1 = mixedValues.filter(
x => x !== undefined && x !== null);
// %inferred-type: (number | null | undefined)[]
const stillMixed2 = mixedValues.filter(
x => typeof x === 'number');TypeScript では、独自のタイプガードを定義できます。たとえば、次のようになります。
function isFunction(value: unknown): value is Function {
return typeof value === 'function';
}戻り値の型 value is Function は *型述語* です。これは isFunction() の型シグネチャの一部です。
// %inferred-type: (value: unknown) => value is Function
isFunction;ユーザー定義タイプガードは常にブール値を返す必要があります。 isFunction(x) が true を返すと、TypeScript は実引数 x の型を Function に絞り込みます。
function func(arg: unknown) {
if (isFunction(arg)) {
// %inferred-type: Function
arg; // type is narrowed
}
}TypeScript は、ユーザー定義タイプガードの結果をどのように計算するかは気にしないことに注意してください。これにより、使用するチェックに関して大きな自由度が得られます。たとえば、isFunction() は次のように実装することもできます。
function isFunction(value: any): value is Function {
try {
value(); // (A)
return true;
} catch {
return false;
}
}残念ながら、unknown 型では行 A の関数呼び出しを実行できないため、パラメータ value に any 型を使用する必要があります。
isArrayWithInstancesOf()/**
* This type guard for Arrays works similarly to `Array.isArray()`,
* but also checks if all Array elements are instances of `T`.
* As a consequence, the type of `arr` is narrowed to `Array<T>`
* if this function returns `true`.
*
* Warning: This type guard can make code unsafe – for example:
* We could use another reference to `arr` to add an element whose
* type is not `T`. Then `arr` doesn’t have the type `Array<T>`
* anymore.
*/
function isArrayWithInstancesOf<T>(
arr: any, Class: new (...args: any[])=>T)
: arr is Array<T>
{
if (!Array.isArray(arr)) {
return false;
}
if (!arr.every(elem => elem instanceof Class)) {
return false;
}
// %inferred-type: any[]
arr; // (A)
return true;
}行 A では、arr の推論された型が Array<T> *ではない* ことがわかりますが、チェックにより、現在はそうなっています。そのため、true を返すことができます。TypeScript は私たちを信頼し、isArrayWithInstancesOf() を使用すると Array<T> に絞り込みます。
const value: unknown = {};
if (isArrayWithInstancesOf(value, RegExp)) {
// %inferred-type: RegExp[]
value;
}isTypeof()これは、TypeScript で typeof を実装するための最初の試みです。
/**
* An implementation of the `typeof` operator.
*/
function isTypeof<T>(value: unknown, prim: T): value is T {
if (prim === null) {
return value === null;
}
return value !== null && (typeof prim) === (typeof value);
}理想的には、文字列 (つまり、typeof の結果の 1 つ) を介して value の予期される型を指定できるはずです。しかし、その場合、その文字列から型 T を派生させる必要があり、その方法がすぐにはわかりません (後ほど説明するように、方法があります)。回避策として、T のメンバー prim を介して T を指定します。
const value: unknown = {};
if (isTypeof(value, 123)) {
// %inferred-type: number
value;
}より良い解決策は、オーバーロードを使用することです (いくつかのケースは省略されています)。
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof(value: any, typeString: 'boolean'): value is boolean;
function isTypeof(value: any, typeString: 'number'): value is number;
function isTypeof(value: any, typeString: 'string'): value is string;
function isTypeof(value: any, typeString: string): boolean {
return typeof value === typeString;
}
const value: unknown = {};
if (isTypeof(value, 'boolean')) {
// %inferred-type: boolean
value;
}(このアプローチは、Nick Fisher によるアイデアです。)
別の方法として、インターフェースを文字列から型へのマップとして使用する方法があります (いくつかのケースは省略されています)。
interface TypeMap {
boolean: boolean;
number: number;
string: string;
}
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof<T extends keyof TypeMap>(value: any, typeString: T)
: value is TypeMap[T] {
return typeof value === typeString;
}
const value: unknown = {};
if (isTypeof(value, 'string')) {
// %inferred-type: string
value;
}(このアプローチは、Ran Lottem によるアイデアです。)
アサーション関数は、パラメータが特定の基準を満たしているかどうかを確認し、満たしていない場合は例外をスローします。たとえば、多くの言語でサポートされているアサーション関数の1つは、assert()です。 assert(cond)は、ブール条件condがfalseの場合に例外をスローします。
Node.jsでは、assert()は組み込みモジュールassertを介してサポートされています。次のコードは、A行でそれを使用しています
import assert from 'assert';
function removeFilenameExtension(filename: string) {
const dotIndex = filename.lastIndexOf('.');
assert(dotIndex >= 0); // (A)
return filename.slice(0, dotIndex);
}TypeScriptの型推論は、アサーション関数を_アサーションシグネチャ_を戻り値の型としてマークすると、特別なサポートを提供します。関数の戻り値とその方法に関して、アサーションシグネチャはvoidと同等です。ただし、さらに絞り込みをトリガーします。
アサーションシグネチャには2種類あります
asserts «cond»asserts «arg» is «type»asserts «cond»次の例では、アサーションシグネチャasserts conditionは、パラメータconditionがtrueでなければならないことを示しています。そうでない場合は、例外がスローされます。
function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error();
}
}assertTrue()は、このように絞り込みを行います
function func(value: unknown) {
assertTrue(value instanceof Set);
// %inferred-type: Set<any>
value;
}引数value instanceof Setを型ガードと同様に使用していますが、条件文の一部をスキップする代わりに、falseが例外をトリガーします。
asserts «arg» is «type»次の例では、アサーションシグネチャasserts value is numberは、パラメータvalueが型numberでなければならないことを示しています。そうでない場合は、例外がスローされます。
function assertIsNumber(value: any): asserts value is number {
if (typeof value !== 'number') {
throw new TypeError();
}
}今回は、アサーション関数を呼び出すと、引数の型が絞り込まれます
function func(value: unknown) {
assertIsNumber(value);
// %inferred-type: number
value;
}関数addXY()は、既存のオブジェクトにプロパティを追加し、それに応じて型を更新します
function addXY<T>(obj: T, x: number, y: number)
: asserts obj is (T & { x: number, y: number }) {
// Adding properties via = would be more complicated...
Object.assign(obj, {x, y});
}
const obj = { color: 'green' };
addXY(obj, 9, 4);
// %inferred-type: { color: string; } & { x: number; y: number; }
obj;交差型S & Tは、型Sと型Tの両方のプロパティを持ちます。
function isString(value: unknown): value is string {
return typeof value === 'string';
}value is stringbooleanasserts «cond»function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error(); // assertion error
}
}asserts conditionvoid、例外asserts «arg» is «type»function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(); // assertion error
}
}asserts value is stringvoid、例外アサーション関数は、既存の値の型を絞り込みます。強制変換関数は、新しい型を持つ既存の値を返します。次に例を示します
function forceNumber(value: unknown): number {
if (typeof value !== 'number') {
throw new TypeError();
}
return value;
}
const value1a: unknown = 123;
// %inferred-type: number
const value1b = forceNumber(value1a);
const value2: unknown = 'abc';
assert.throws(() => forceNumber(value2));対応するアサーション関数は次のようになります
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new TypeError();
}
}
const value1: unknown = 123;
assertIsNumber(value1);
// %inferred-type: number
value1;
const value2: unknown = 'abc';
assert.throws(() => assertIsNumber(value2));強制変換は、アサーション関数の用途以外にも使用できる汎用的な手法です。たとえば、次のように変換できます
詳細については、[コンテンツは含まれていません]を参照してください。
次のコードについて考えてみましょう
function getLengthOfValue(strMap: Map<string, string>, key: string)
: number {
if (strMap.has(key)) {
const value = strMap.get(key);
// %inferred-type: string | undefined
value; // before type check
// We know that value can’t be `undefined`
if (value === undefined) { // (A)
throw new Error();
}
// %inferred-type: string
value; // after type check
return value.length;
}
return -1;
}A行で始まるif文の代わりに、アサーション関数を使用することもできます
assertNotUndefined(value);そのような関数を記述したくない場合は、例外をスローするのが簡単な代替手段です。アサーション関数を呼び出すのと同様に、この手法も静的型を更新します。
@hqoss/guards: 型ガード付きライブラリライブラリ@hqoss/guardsは、TypeScript用の型ガードのコレクションを提供します。次に例を示します
isBoolean()、isNumber()などisObject()、isNull()、isFunction()などisNonEmptyArray()、isInteger()など