始めに
自分が今までTypeScriptで型推論させるにあたって、中々いい方法が見つからず、他にいいやり方がないか模索して時間がかかってしまうケースがいくつかありました。
そこで今回は割と使うケースで解決するのに苦労したものについて、Tipsという形でまとめてみました。自分はこのやり方でやっていますが、他にいいやり方があれば是非コメントください!
基本編と上級編に分けており、上級編の方が割と込み入ったことをやっているものになります。
基本編
Object.keysに型をつける
Object.keys
の返り値の型はstring
で固定されているため、以下のように書くとtype errorになってしまいます。
const obj = {
a: 10,
b: 'text',
c: true,
};
Object.keys(obj).forEach((key) => {
// key: string なのでエラーになってしまう
console.log(key, obj[key]);
});
そこでkeyのunion typeが返ってくるようなメソッドでラップします。
function typedKeys<T>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}
typedKeys(obj).forEach((key) => {
// key: 'a' | 'b' | 'c' となるのでOK
console.log(key, obj[key]);
});
余談
一応ObjectConstructor
を型拡張することでObject.keys
でも詳細の型が当たるようになりますが、僕はなんとなく直接いじるのは良くなさそうな気がしたので上記のようにラップしたメソッドを使っています。
interface ObjectConstructor {
// Object.keysの型を上書く
keys<T>(obj: T): Array<keyof T>;
}
Object.keys(obj).forEach((key) => {
// key: 'a' | 'b' | 'c' となるのでOK
console.log(obj[key])
})
サンプルコード
filterでnullの型を除去したい時
filterでnullを除去したのに、型はnullも入ったままになってしまいます。
const arr = [1, 2, 3, null, null];
// filterでnullは取り除いても型はArray<number | null>
const result = arr.filter((item) => item != null);
is
を使ったtype guardを使うとnullを取り除いた結果を返すことができます。
// Array<number>になる
const result = arr.filter((item): item is number => item != null);
ただこれを毎回書くのは手間なので、filterHandlerを用意することでnullの型を除去した内容が返ってくるようにできます。
function omitNullableHandler<T>(item: T): item is NonNullable<T> {
return item != null;
}
// nullが取り除かれてArray<number>になる
const result = arr.filter(omitNullableHandler)
サンプルコード
union typeを配列にしたい場合
type Direction = 'east' | 'west' | 'south' | 'north'
のように先にunion typeで型定義をして、それを網羅した配列を作りたいケースです。
残念ながらこのやり方は存在しませんが、逆に先に配列を定義して、その後union typeを生成する流れはできます。
const directionList = ['east', 'west', 'south', 'north'] as const;
// 'east' | 'west' | 'south' | 'north'
type Direction = typeof directionList[number];
サンプルコード
enumを使いたい場合
TypeScriptにはenumがありますが、あまりしっかりとしていなくて、enum値以外も代入できてしまう問題があります。
enum Direction {
Up,
Right,
Down,
Left,
};
// enumの型で、enumに入っている値を入れる
const dir: Direction = Direction.Up;
// 範囲外のenum値でも代入できてしまうorz
const dir2: Direction = 10;
これ以外にもenumには問題があるらしく、TypeScriptでenum的なことをする場合は以下のように一度オブジェクトで定義してからunion typeに変換するのが良いです。ちょっと冗長で分かりづらいところもあると思いますが、型安全のことを考えるとこれが一番だと思ってます。
const Direction = {
Up: 0,
Right: 1,
Down: 2,
Left: 3,
} as const;
// constと同じ名前で型定義できる
type Direction = typeof Direction[keyof typeof Direction];
// 使い方はenumと変わらないがTSとJS空間が分かれていて、知らないと混乱するかも
// 左のDirection: TS空間のtype
// 右のDirection: JS空間のconst
const dir: Direction = Direction.Up;
// constで定義した値以外を弾ける
const dir2: Direction = 10;
サンプルコード
参考
オブジェクトの型から1つだけキーを取り除きたい場合
他に代入する都合で、不要なキーだけ取り除きたいが、改めて型定義するのは面倒な場合です。
意外と簡単で、スプレッド構文で残りを受け取るだけで勝手に型推論してくれます(便利!)
const obj = {
a: 10,
b: 'text',
c: true,
};
const { a, ...restObj } = obj;
// restObjにはb, cの内容だけ残る
console.log(restObj);
サンプルコード
上級編
union typeをループさせたい場合
リストデータをまとめて出力するメソッドを作る際に、細かい調整をparserメソッドを通してやれないかと思った時です。
const data = {
price: 1000,
enabled: true,
text: 'hogehoge',
};
const items = [
{
label: '金額',
key: 'price',
// ここのvalueはdata.priceで定義されたnumberで推論されたい
parser: (value) => value.toLocaleString(),
},
{
label: '可否フラグ',
key: 'enabled',
// ここのvalueはdata.enabledで定義されたbooleanで推論されたい
parser: (value) => value ? '可' : '否',
},
{
label: 'テキスト',
key: 'text',
},
];
function outputList(data, items) {
items.forEach((item) => {
const text = item.parser ? item.parser(data[item.key]) : data[item.key];
console.log(item.label, text);
});
}
outputList(data, items);
これに型を書こうとすると結構難しく、直接型を書くと以下のようにできますが、なんとかループで回して汎用的にしたいところです。
type Item<T, K extends keyof T> = {
label: string;
key: K;
parser?: (value: T[K]) => string;
}
type SpecifyItem =
// keyofで得られた内容をループでunion typeに変換したい
| Item<typeof data, 'price'>
| Item<typeof data, 'enabled'>
| Item<typeof data, 'text'>;
type SpecifyItems = SpecifyItem[];
そのやり方はあるようで、以下の記事を参考にして、ループで書くことができました。
Literal TypeのUnion Typeをリストのように使用する
type MappedTypeItem<T> = { [K in keyof T]: Item<T, K> }[keyof T];
// MappedTypeItem<typeof data> = {
// price: Item<typeof data, 'price'>;
// enabled: Item<typeof data, 'enabled'>;
// text: Item<typeof data, 'text'>;
// }['price' | 'enabled' | 'text']
// MappedTypeItem<typeof data> == SpecifyItem になる
サンプルコード
大枠の型を決めつつ、詳細の型は宣言したものにしたい場合
例えばvuexのようにmutationsを定義する際に先に型を指定することで推論する時、便利ですが逆にその型で丸められてしまい、mutation名をkeyofで取得できなくなる問題があります。
// vuexのmutationsの型定義
type Mutation<S> = (state: S, payload?: any) => any;
interface MutationTree<S> {
[key: string]: Mutation<S>;
}
type State = {
count: number;
}
const mutations: MutationTree<State> = {
// stateが推論される
add(state, value: number) {
state.count += value;
},
sub(state, value: number) {
state.count -= value;
}
};
// 'add' | 'sub'が欲しいが、string | numberになってしまう
type mutationName = keyof typeof mutations;
型を先に決めておいて、最終結果は宣言したものにしたいというなかなか無茶な要求ですが、クロージャを使って上手いことやることができます。
// Tに期待する型を先に書く
function typeChecker<T>() {
// 推論されるUはTの型に内包されているかチェック
return function check<U extends T>(checkVar: U) {
return checkVar;
};
}
const mutations = typeChecker<MutationTree<State>>()({
// 推論もちゃんと効く
add(state, value: number) {
state.count += value;
},
sub(state, value: number) {
state.count -= value;
}
});
// ちゃんと 'add' | 'sub' が出てくる
type mutationName = keyof typeof mutations;
サンプルコード
詳細
ほとんどここで書いた内容が全てですが、こちらにも書いてありますので、興味がある方は読んでください。
終わりに
以上が型推論のTips集でした。結構実装に悩んだものばかりですが、汎用的になっているので結構参考になれる部分が多いかなと思っています。似たようなケースにぶち当たった時に参考になれれば幸いです。