TypeScript の Type Guard 便利ですよね。↓のようなやつ。
const concat = (arr?: string[]) => {
// arr の型は string[] | undefined なので↓はコンパイルエラーとなる。
// 「Object is possibly 'undefined'.」
// arr.join('');
if (arr) {
// このブロック内は arr の型は string[] であると見なされるので
// ↓はコンパイルできる。
return arr.join('');
}
return '';
};
でもコンパイラオプションの strictNullChecks
がオンの場合 (TypeScript を使うならほとんどの場合でオンだと思いますが)、使い勝手が悪かったりします。例えばこんな感じ。
const concatArray = (arr1?: string[], arr2?: string[]) => {
const canConcat = arr1 && arr2;
// arr1, arr2 のどちらも undefined ではないことを確認
if (canConcat) {
// でも↓はコンパイルエラー!
// 「Type 'string[] | undefined' is not an array type or a string type.」
return [...arr1, ...arr2];
}
return [];
};
上記例の if ブロックの中では arr1
arr2
ともに truthy ですが、コンパイルは通りません。でも canConcat
の判定は使いまわしたいし・・・
このような場合でも “User-Defined Type Guards” と呼ばれる方法を使うとスマートに書くことができます。
User-Defined Type Guards
公式はこちら。
つまり組み込み型だけではなくて独自型でも Type Guard が効くようにできる、ということです。
構文
通常の boolean を返す関数です。ただし、返却値の型として “Type Predicate” と呼ばれるものを指定します。
// アロー関数で書くならこう
const isStringArray = (arg1: string | string[]): arg1 is string[] => {
return arg1 instanceof Array;
};
// function で書くならこう
function isStringArray(arg1: string | string[]): arg1 is string[] {
return arg1 instanceof Array;
}
arg1 is string[]
の部分が Type Predicate です。この isStringArray
を使うとこんなふうに書けます。
// strOrArr の型が string[] だったら join
const str: string = isStringArray(strOrArr) ? strOrArr.join("\n") : strOrArr;
何が便利なの?
上記の concatArray
の例のように、ちょっと複雑な条件判定をしつつ Type Guard を効かせたい時に便利です。
interface ArrayPair {
arr1: string[];
arr2: string[];
}
type PartialArrayPair = Partial<ArrayPair>;
const hasArrays = (arrays: PartialArrayPair): arrays is ArrayPair => {
return arrays.arr1 !== undefined && arrays.arr2 !== undefined;
}
const a1 = ["a", "b"];
const a2 = ["c", "d"];
if (hasArrays({ arr1: a1, arr2: a2 })) {
const newArr = [...a1, ...a2]; // ["a", "b", "c", "d"]
}
if (hasArrays) ....
の中では a1
a2
の型は string[]
であることが保証されます。
この例はプロパティが2つしかないし条件判定も1回だけなので、hasArrays
を用意する必要はないと思いますが・・・もっと巨大なオブジェクトの判定を何回も行わなければいけないとなると・・・こういう機構が必要になりますね。
Type Predicate の注意
Type Predicate の右辺 (is
の右に書く型) は、引数の型のサブセットである必要があります。
interface Person {
name: string;
age?: number;
}
// コンパイルエラー!
// 「A type predicate's type must be assignable to its parameter's type.」
const hasAge = (p: Person): p is { age: number } => {
return p.age !== undefined;
}
この例の場合 { age: number }
は name
を持っていないため、関数が true を返したとしても p
は Person
に割り当てできません。
この場合はこんなふうにすれば OK です。
const hasAge = (p: Person): p is Person & { age: number } => {
return p.age !== undefined;
}
Person & { age: number }
という型は実際には以下のようになり、Person
のサブセットとなります。
// Person & { age: number }
{
name: string;
age: number
}