i18next.t() の引数をTemplate Literal Types で縛る

avatar

Yuji Matsumoto / April 30 2021

i18next とは

i18nextは国際化対応を行うためのライブラリです。init 時に対応したい locale の辞書を読み込ませた上で、i18n.t()に辞書の key を渡すと、locale に基づいた value を返却してくれます。

import i18next from 'i18next';

const en = {
  address: 'address',
  animals: {
    cat: 'cat',
  },
};

const ja = {
  address: '住所',
  animals: {
    cat: '猫',
  },
};

const resources = {
  'en-US': {
    translation: en,
  },
  ja: {
    translation: ja,
  },
};

i18next.init({
  lng: 'ja',
  resources,
});

i18next.t('address');
// => ja だと住所
// => en-US だとaddress

i18next.t('animals.cat');
// => ja だと猫
// => en-US だとcat

i18next.t()が受けとる key の型は string|string[] になっています。今回はここの型を実際に存在する辞書の key のみに縛り、辞書に存在しない key を指定する事故を防ぐ & IDE 上で補完が効く状態を目指します。

string の value を持った key を取り出す型を定義する

type RecursiveRecord = {
  [key in string]: string | RecursiveRecord;
};

type PickKeys<T extends RecursiveRecord, K = keyof T> = K extends string
  ? T[K] extends string
    ? K
    : `${K}.${PickKeys<Extract<T[K], RecursiveRecord>>}`
  : never;

RecursiveRecordstringRecursiveRecord 自身を value として保持する object の型で、辞書ファイルの型を想定しています。PickKeysRecursiveRecord から string の value を持った key を取り出す型です。T[K] が string の場合は、そのまま key を返し、T[K]RecursiveRecord の場合は、key と、value の object をPickKeys に通したものを . で join するような template literal を返します。このPickKeys 型に辞書ファイルの型を generics で渡してあげれば、i18next.t() の引数として有効な key の union を得ることができます。

type I18nDictionary = {
  address: string;
  animals: {
    cat: string;
    dog: string;
  };
};

type I18nKey = PickKeys<I18nDictionary>;

wrapper function を作成する

先程定義したI18nKey 型を使って、i18next.t() の wrapper function を定義します。

import i18n, { TFunctionResult, TOptions } from 'i18next';

const translate = (key: I18nKey | I18nKey[], options?: TOptions) => {
  return i18next.t<TFunctionResult, I18nKey>(key, options);
};

translate() の引数を入力する際に、keyの型が補完されるようになりました。