typescript - 基于配置对象智能强制参数类型

标签 typescript

我不知道该怎么调用它,所以在我描述它时请裸露我的。

我有三个配置对象:

const configA = {
    type: 'A' as const,
    getPath: (query: { foo: string }) => `/${query.foo}`
}

const configB = {
    type: 'B' as const,
    getPath: (query: {}) => `/path/b`
}

const configC = {
    type: 'C' as const,
    getPath: (query: {bar: number}) => `/c/${query.bar}`
}

我将这些合并到一个主记录中,如下所示:

const CONFIGS = {
    [configA.type]: configA,
    [configB.type]: configB,
    [configC.type]: configC
}

我现在想编写一个函数,其参数遵循上面的配置。这样就提供了类型安全。因此,如果我这样做的话,上面的内容是:

add({
    type: 'C',
    query: { foo: true }
})

Typescript 应该警告我,如果 typeC,则此处的 query 对象无效。然而它并没有警告我。

我尝试过这个:

function add(inputs: {
    type: (typeof CONFIGS)[keyof typeof CONFIGS]['type'],
    query: Parameters<(typeof CONFIGS)[keyof typeof CONFIGS]['getPath']>[0]
}) {
 const config = CONFIGS[inputs.type];
 if (!config) {
    throw new Error('Config not found');
 }

 const url = 'mysite.blah' + config.getPath(inputs.query); // inputs.query is getting Typescript warning

 console.log('url:', url);
}

但是它不起作用,并且还有一个额外的问题,即我执行 config.getPath(inputs.query) 的行上的 inputs.query 抛出此 Typescript错误:

Argument of type '{} | { foo: string; } | { bar: number; }' is not assignable to parameter of type '{ foo: string; } & { bar: number; }'.

这是我的完整代码:TypeScript Playground

这种技术叫什么?有可能解决这个问题吗?谢谢。

最佳答案

add 的调用签名不是类型安全的,因为 inputstypequery 属性参数之间是相互独立的。两者都是 union types其中每个都可以是(三个)可能值中的任何一个。您可以通过将inputs 的类型设置为单个联合来解决此问题,以便typequery 相互关联:

type Inputs = {
    [P in keyof typeof CONFIGS]: {
        type: P,
        query: Parameters<typeof CONFIGS[P]["getPath"]>[0]
    }
}[keyof typeof CONFIGS]

/* type Inputs = 
  { type: "A"; query: { foo: string; }; } | 
  { type: "B"; query: {}; } | 
  { type: "C"; query: { bar: number; }; } */

declare function add(inputs: Inputs): void;

add({ type: 'C', query: { foo: true } }); // error!
add({ type: 'C', query: { bar: 123 } }); // okay

这从调用者的角度解决了您的问题。不幸的是,实现本质上有相同的错误:

function add(inputs: Inputs) {
    const config = CONFIGS[inputs.type];
    if (!config) {
        throw new Error('Config not found');
    }
    const url = 'mysite.blah' + config.getPath(inputs.query); // error!
    console.log('url:', url);
};

这是因为 TypeScript 缺乏对我所说的“相关联合”的直接支持,如 microsoft/TypeScript#30581 中所述。 。当编译器类型检查单个代码块(例如 add() 主体)时,它会执行一次。仅从体内表达式的类型来看,没有足够的信息来验证您所做的事情是否安全。是的,inputs 必须是正确的类型,但是当您尝试使用它两次(例如 CONFIGS[inputs.type].getPath(inputs.query))时,编译器无法跟随线程。如果它可以分析该代码三次,每次联合缩小一次,它就会起作用。但它只能执行一次。

microsoft/TypeScript#47109 中描述了处理此问题的推荐方法。并涉及重构,以便用简单的键值类型和 generic 来表达操作,而不是联合。 indexes进入该类型并进入 mapped types超过那种类型。目标是使 CONFIGS[input.type].getPath 被视为单个函数(而不是联合),其参数与 input.query< 的参数具有相同的泛型类型.

重构可能如下所示:

首先让我们重命名 CONFIGS,因为我们需要更改它的类型:

const _CONFIGS = {
    [configA.type]: configA,
    [configB.type]: configB,
    [configC.type]: configC
}

然后我们将使用它来构建我们的简单键值类型:

type QueryMap = { [K in keyof typeof _CONFIGS]: 
  Parameters<typeof _CONFIGS[K]["getPath"]>[0] 
}

/* type QueryMap = {
    A: { foo: string; };
    B: {};
    C: { bar: number;};
} */

我们将使用 QueryMap 类型作为此处涉及的关系的基础。您可以从手动编写的类型开始,并根据它定义 CONFIGS,但这更容易扩展,因为如果您添加成员,它会自动更新。

现在我们将 CONFIGS 的类型重写为 QueryMap 上的映射类型:

type Configs = { [K in keyof QueryMap]: { type: K, getPath: (query: QueryMap[K]) => string } };

并说 CONFIGS 属于这种类型:

const CONFIGS: Configs = _CONFIGS; // okay

赋值成功,因此编译器能够看到typeof _CONFIGSConfigs之间的等价性。现在我们可以将 add() 重写为 K extends keyof Configs 中的通用函数:

function add<K extends keyof Configs>(inputs: { type: K, query: QueryMap[K] }) {
    const config = CONFIGS[inputs.type];
    if (!config) {
        throw new Error('Config not found');
    }

    const url = 'mysite.blah' + config.getPath(inputs.query); // okay

    console.log('url:', url);
}

现在可以了。这很微妙,因为如果您在函数第一行中将 CONFIGS 替换为 _CONFIGS ,错误将会再次出现。是的,它们的类型是等效的,但编译器对它们的处理方式不同。 Configs 类型表示为 QueryMap 上的映射类型,并且 inputs 也具有依赖于 QueryMap 的类型,这个对应关系可用于类型检查功能。 _CONFIGS 的类型未针对 QueryMap 进行表达,因此编译器看不到它与输入之间的通用连接。

哦,调用者仍然会得到您想要的行为:

add({ type: 'C', query: { foo: true } }); // error!
add({ type: 'C', query: { bar: 123 } }); // okay

给你。此重构与 add() 实现的编译器验证的类型安全性非常接近。我不确定这是否一定值得;如果您不关心编译器在实现中检查您的代码,您可以使用 type assertion使其静音:

function add(inputs: Inputs) {
    const config = CONFIGS[inputs.type];
    if (!config) {
        throw new Error('Config not found');
    }
    const url = 'mysite.blah' + config.getPath(inputs.query as any); // okay
    console.log('url:', url);
};

这更容易做到。这完全取决于您的用例。

Playground link to code

关于typescript - 基于配置对象智能强制参数类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76239187/

相关文章:

mongodb - Angular 5 + Material 设计 : <mat-select> how to set the default value?

angular - 失败 : Template parse errors: Angular 2

typescript - 基于非空字符串的条件类型

angular - 警告 : sanitizing unsafe style value

javascript - import qs 未定义,但适用于 require

typescript - VS Code 提示错误 TS7013 但 Typescript 不是

angular - 文件删除上传数据传输为空Angular 2

javascript - useQuery 的 onError 回调函数(react-query)中错误对象为 null

angular - 刷新数据和分页状态

typescript 返回类型取决于参数