59
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

始めに

自分が今まで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集でした。結構実装に悩んだものばかりですが、汎用的になっているので結構参考になれる部分が多いかなと思っています。似たようなケースにぶち当たった時に参考になれれば幸いです。

59
47
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?