首页 > 解决方案 > 打字稿根据前一个参数的值推断可能的对象键?

问题描述

我正在尝试建立翻译服务。用于翻译标签的函数应该对可能的标签提供类型检查,但也应该对基于给定标签的替换对象提供类型检查。

我有一个标签列表对象,它描述了可能的标签和一个应该出现在翻译字符串中的占位符列表。

export const TagList = {
    username_not_found: ['username']
};

在这种情况下,关键是字典应该实现的标签名称。该值是应出现在已翻译字符串中的占位符列表。

字典看起来有点像这样:

// Note: The type declaration of keys don't work this way (key should be number or string). Not sure how I should implement this...
const en: {[name: keyof (typeof TagList)]: string} = {
    "username_not_found": "The user {username} does not exist"
}

用于翻译标签的方法如下:

this.trans("username_not_found", {username: "someone@example.com"});

我想要实现的是在我的 IDE 中对占位符对象进行类型检查(自动完成),以强制配置所有占位符。

例如:

// This is wrong: "username" placeholder is not specified.
this.trans("username_not_found", {});

// This is also wrong: "foobar" is a non-existing placeholder.
this.trans("username_not_found", {foobar: "42"});

// This is good:
this.trans("username_not_found", {username: "someone@example.com"});

目前我正在keyof (typeof TagList)用作tagName. 我不确定这是否是正确的方法,但它确实有效。我现在正在寻找一种方法来根据第一个参数中给出的值推断第二个参数的对象结构。

我试图避免维护多个可能的标签列表(例如,必须同时在接口和对象中声明它们)。

提前致谢!

标签: typescripttypescript-typingstypescript-generics

解决方案


首先,您需要使 TagList 不可变。

然后我只是根据键创建了文字类型。非常相似Array.prototype.reduce

export const TagList = {
    username_not_found: ['username'],
    username_found: ['name'],
    batman: ['a', 'b']
} as const;

type Elem = string

type Reducer<
    Arr extends ReadonlyArray<Elem>, // array
    Result extends Record<string, any> = {} // accumulator
    > = Arr extends [] ? Result // if array is empty -> return Result
    : Arr extends readonly [infer H, ...infer Tail] // if array is not empty, do recursive call with array Tail and Result
    ? Tail extends ReadonlyArray<Elem>
    ? H extends Elem
    ? Reducer<Tail, Result & Record<H, string>>
    : never
    : never
    : never;

type TagList = typeof TagList;

const trans = <Tag extends keyof TagList, Props extends Reducer<TagList[Tag]>>(tag: Tag, props: Props) => null as any

trans("username_not_found", { username: "someone@example.com" }); // ok
trans("username_found", { name: "John" }); // ok
trans("batman", { a: "John", b: 'Doe' }); // ok

trans("username_not_found", { name: "someone@example.com" }); // expected error
trans("username_found", { username: "John" }); // expected error

这里的主要目标是将元组转换['username']{ username: string }

你会如何在纯js中做到这一点?

['username'].reduce((acc, elem) => ({ ...acc, [elem]: 'string' }), {})

我使用几乎相同的算法,但使用递归而不是迭代。

这是Reducer实用程序类型的 js 类似物

const reducer = (arr: ReadonlyArray<Elem>, result: Record<string, any> = {}): Record<string, any> => {
    if (arr.length === 0) {
        return result
    }

    const [head, ...tail] = arr;

    return reducer(tail, { ...result, [head]: 'string' })
}

您可以在我的博客中找到更多信息


推荐阅读