如何使用 TypeScript 键入检查 i18n 词典?

Gre*_*een 10 typescript i18next react-i18next

是否可以在react-i18next字典中键入检查现有键?因此,如果密钥不存在,TS 会在编译时警告您。

例子。

假设,我们有这本字典:

{
  "footer": {
    "copyright": "Some copyrights"
  },

  "header": {
    "logo": "Logo",
    "link": "Link",
  },
}
Run Code Online (Sandbox Code Playgroud)

如果我提供不存在的密钥,TS 应该会爆炸:

const { t } = useTranslation();

<span> { t('footer.copyright') } </span> // this is OK, because footer.copyright exists
<span> { t('footer.logo') } </span> // TS BOOM!! there is no footer.logo in dictionary
Run Code Online (Sandbox Code Playgroud)

这种技术的正确名称是什么?我很确定我不是唯一一个要求这种行为的人。

它是react-i18next开箱即用的吗?是否有 APIreact-i18next以某种方式扩展库以启用它?我想避免创建包装函数。

for*_*d04 21

TS 4.1

最后通过模板文字类型支持类型化字符串键查找和插值。

我们现在可以使用虚线字符串参数来深入访问字典键/对象路径:

t("footer"); // ? { copyright: "Some copyrights"; }
t("footer.copyright"); // ? "Some copyrights"
t("footer.logo"); // ? should trigger compile error
Run Code Online (Sandbox Code Playgroud)

让我们看看1.)翻译函数的合适返回类型t 2.)我们如何在不匹配的键参数上发出编译错误并提供 IntelliSense 3.)字符串插值示例。

1. 关键字查找:返回类型

// returns property value from object O given property path T, otherwise never
type GetDictValue<T extends string, O> =
    T extends `${infer A}.${infer B}` ? 
    A extends keyof O ? GetDictValue<B, O[A]> : never
    : T extends keyof O ? O[T] : never

function t<P extends string>(p: P): GetDictValue<P, typeof dict> { /* impl */ }
Run Code Online (Sandbox Code Playgroud)

操场

2. 关键查找:IntelliSense 和编译错误

在错误的键上触发编译错误可能就足够了:

// returns the same string literal T, if props match, else never
type CheckDictString<T extends string, O> =
  T extends `${infer A}.${infer B}` ?
  A extends keyof O ? `${A}.${Extract<CheckDictString<B, O[A]>, string>}` :never
  : T extends keyof O ? T : never

function t<P extends string>(p: CheckDictString<P, typeof dict>)
  : GetDictValue<P, typeof dict> { /* impl */ }
Run Code Online (Sandbox Code Playgroud)

操场

如果您还需要IntelliSense,请继续阅读。以下类型将查询字典的所有可能的键路径排列,提供自动完成并协助不匹配键的错误提示:

// get all possible key paths
type DeepKeys<T> = T extends object ? {
    [K in keyof T]-?: `${K & string}` | Concat<K & string, DeepKeys<T[K]>>
}[keyof T] : ""

// or: only get leaf and no intermediate key path
type DeepLeafKeys<T> = T extends object ?
    { [K in keyof T]-?: Concat<K & string, DeepKeys<T[K]>> }[keyof T] : "";

type Concat<K extends string, P extends string> =
    `${K}${"" extends P ? "" : "."}${P}`
Run Code Online (Sandbox Code Playgroud)
function t<P extends DeepKeys<typeof dict>>(p: P) : GetDictValue<P, typeof dict> 
  { /* impl */ } 

type T1 = DeepKeys<typeof dict> 
// "footer" | "header" | "footer.copyright" | "header.logo" | "header.link"
type T2 = DeepLeafKeys<typeof dict> 
// "footer.copyright" | "header.logo" | "header.link"
Run Code Online (Sandbox Code Playgroud)

操场

有关更多详细信息,请参阅Typescript:嵌套对象的深度 keyof

由于组合复杂性和取决于字典对象的形状,您可能会遇到编译器递归深度限制。更轻量级的替代方案:根据当前输入为下一个关键路径增量提供 IntelliSense :

// T is the dictionary, S ist the next string part of the object property path
// If S does not match dict shape, return its next expected properties 
type DeepKeys<T, S extends string> =
    T extends object
    ? S extends `${infer I1}.${infer I2}`
        ? I1 extends keyof T
            ? `${I1}.${DeepKeys<T[I1], I2>}`
            : keyof T & string
        : S extends keyof T
            ? `${S}`
            : keyof T & string
    : ""

function t<S extends string>(p: DeepKeys<typeof dict, S>)
  : GetDictValue<S, typeof dict> { /* impl */ }

t("f"); // error, suggests "footer"
t("footer"); // OK
t("footer."); // error, suggests "footer.copyright"
t("footer.copyright"); // OK
t("header.") // error, suggests "header.logo" | "header.link"
Run Code Online (Sandbox Code Playgroud)

操场

3. 插值

这是一个使用字符串 插值的示例。

// retrieves all variable placeholder names as tuple
type Keys<S extends string> = S extends '' ? [] :
    S extends `${infer _}{{${infer B}}}${infer C}` ? [B, ...Keys<C>] : never

// substitutes placeholder variables with input values
type Interpolate<S extends string, I extends Record<Keys<S>[number], string>> =
    S extends '' ? '' :
    S extends `${infer A}{{${infer B}}}${infer C}` ?
    `${A}${I[Extract<B, keyof I>]}${Interpolate<C, I>}`
    : never
Run Code Online (Sandbox Code Playgroud) 例子:
type Dict = { "key": "yeah, {{what}} is {{how}}" }
type KeysDict = Keys<Dict["key"]> // type KeysDict = ["what", "how"]
type I1 = Interpolate<Dict["key"], { what: 'i18next', how: 'great' }>;
// type I1 = "yeah, i18next is great"

function t<
    K extends keyof Dict,
    I extends Record<Keys<Dict[K]>[number], string>
>(k: K, args: I): Interpolate<Dict[K], I> { /* impl */ }

const ret = t('key', { what: 'i18next', how: 'great' } as const);
// const ret: "yeah, i18next is great"
Run Code Online (Sandbox Code Playgroud)

操场

注意:所有片段都可以结合使用react-i18next或独立使用。



旧答案

( PRE TS 4.1 ) 强类型键在react-i18next以下情况下是不可能的有两个原因:

1.) TypeScript 无法评估动态或计算出的字符串表达式,例如'footer.copyright',因此footercopyright可以被识别为翻译对象层次结构中的关键部分。

2.)useTranslation不会对您定义的字典/翻译强制执行类型约束。相反,函数t包含默认为 的泛型类型参数string,当未手动指定时。


这是使用Rest parameters/tuples的替代解决方案。

输入t函数:
type Dictionary = string | DictionaryObject;
type DictionaryObject = { [K: string]: Dictionary };

interface TypedTFunction<D extends Dictionary> {
    <K extends keyof D>(args: K): D[K];
    <K extends keyof D, K1 extends keyof D[K]>(...args: [K, K1]): D[K][K1];
    <K extends keyof D, K1 extends keyof D[K], K2 extends keyof D[K][K1]>(
        ...args: [K, K1, K2]
    ): D[K][K1][K2];
    // ... up to a reasonable key parameters length of your choice ...
}
Run Code Online (Sandbox Code Playgroud) 类型useTranslation挂钩:
import { useTranslation } from 'react-i18next';

type MyTranslations = {/* your concrete type*/}
// e.g. via const dict = {...}; export type MyTranslations = typeof dict

// import this hook in other modules instead of i18next useTranslation
export function useTypedTranslation(): { t: TypedTFunction<typeof dict> } {
  const { t } = useTranslation();
  // implementation goes here: join keys by dot (depends on your config)
  // and delegate to lib t
  return { t(...keys: string[]) { return t(keys.join(".")) } }  
}
Run Code Online (Sandbox Code Playgroud)useTypedTranslation在其他模块中 导入:
import { useTypedTranslation } from "./useTypedTranslation"

const App = () => {
  const { t } = useTypedTranslation()
  return <div>{t("footer", "copyright")}</div>
}
Run Code Online (Sandbox Code Playgroud) 测试一下:
const res1 = t("footer"); // const res1: { "copyright": string;}
const res2 = t("footer", "copyright"); // const res2: string
const res3 = t("footer", "copyright", "lala"); // error, OK
const res4 = t("lala"); // error, OK
const res5 = t("footer", "lala"); // error, OK
Run Code Online (Sandbox Code Playgroud)

操场

您可能会自动推断这些类型,而不是多个重载签名 ( Playground )。要知道,这些递归类型不建议在生产的核心开发人员,直到TS 4.1


Aar*_*ary 6

React-i18next 现在对此有内置支持。我找不到官方文档,但源代码中有有用的注释。

假设您的翻译是在public/locales/[locale]/translation.json并且您的主要语言是英语:

// src/i18n-resources.d.ts

import 'react-i18next'

declare module 'react-i18next' {
  export interface Resources {
    translation: typeof import('../public/locales/en/translation.json')
  }
}
Run Code Online (Sandbox Code Playgroud)

如果您使用多个翻译文件,您需要将它们全部添加到资源界面,由命名空间键入。

如果您从 json 文件导入翻译,请确保"resolveJsonModule": true在您的设置中进行设置tsconfig.json

  • 对于使用高于 11.11.0 的 `react-i18next` 版本的任何人,并且您正在关注 https://react.i18next.com/latest/typescript,并且您会遇到 `Module '"react-i18next"' 的问题没有导出成员 'initReactI18next'.` 或 `模块 '"react-i18next"' 没有导出成员 'useTranslations'.` (实际上是任何导入名称),要解决此问题,您所需要做的就是从 ` 重命名您的类型文件正如这个答案所示,react-i18next.d.ts` 到 `i18n-resources.d.ts` 。文档显示的文件名似乎破坏了合并,重命名将修复并保持您的输入。 (3认同)