在TypeScript中指定映射类型的函数参数类型。

huangapple go评论54阅读模式
英文:

Specify function parameter types in TypeScript mapped trype

问题

以下是翻译好的内容:

我有两个函数,它们都接受一个名为 `options` 的单一参数,但具有不同的属性,除了 `type`,它用于标识函数。

```ts
type FuncAOptions = {
    type: 'A'
    opt1: string
    opt2: boolean
}

function funcA(options: FuncAOptions): number {
    if (!options.opt1) throw new Error('Missing required option')
    return 1
}

type FuncBOptions = {
    type: 'B'
    opt3: number
    opt4: (a: number) => number
}

function funcB(options: FuncBOptions): string {
    if (!options.opt3) throw new Error('Missing required option')
    return 'B'
}

然后,我有一个这些函数的映射,具有关联的映射类型,以便我可以根据可变的运行时数据有条件地调用这些函数。

type AllFunctions = FuncAOptions | FuncBOptions

type FunctionMap = { [K in AllFunctions['type']]: (options: any) => any; }

const functionMap: FunctionMap = {
    A: funcA,
    B: funcB
}

function callFunction(type: keyof typeof functionMap, options: any) {
    return functionMap[type](options)
}

当我直接调用这些函数时,我会得到适当的类型检查,以知道是否传递了不正确的选项集。我希望在通过中介方法调用函数时也能做到同样的事情。

callFunction('A', { type: 'B', opt3: 'Hello' }) // 没有 TypeScript 错误
funcA({ type: 'B', opt3: 'Hello' }) // TypeScript 错误:期望的类型来自在 'FuncAOptions' 类型上声明的 'type' 属性

我喜欢使用 K in AllFunctions['type'] 类型的映射,因为当我向 AllFunctions 添加一个函数时,我会被提醒需要向 functionMap 添加键值对。

完整示例链接


<details>
<summary>英文:</summary>

I have two functions that take a single `options` argument with differing properties, except for `type`, which is used to identify the function.

```ts
type FuncAOptions = {
    type: &#39;A&#39;
    opt1: string
    opt2: boolean
}

function funcA(options: FuncAOptions): number {
    if (!options.opt1) throw new Error(&#39;Missing required option&#39;)
    return 1
}

type FuncBOptions = {
    type: &#39;B&#39;
    opt3: number
    opt4: (a: number) =&gt; number
}

function funcB(options: FuncBOptions): string {
    if (!options.opt3) throw new Error(&#39;Missing required option&#39;)
    return &#39;B&#39;
}

I then have a map of these functions, with an associated mapped type, so that I can call the functions conditionally with variable runtime data.

type AllFunctions = FuncAOptions | FuncBOptions

type FunctionMap = { [K in AllFunctions[&#39;type&#39;]]: (options: any) =&gt; any; }

const functionMap: FunctionMap = {
    A: funcA,
    B: funcB
}

function callFunction(type: keyof typeof functionMap, options: any) {
    return functionMap[type](options)
}

When I call the functions directly, I get the proper type-checking to know if I'm passing an incorrect set of options. I want to be able to do the same when calling the function through the intermediary method.

callFunction(&#39;A&#39;, { type: &#39;B&#39;, opt3: &#39;Hello&#39; }) // NO TS ERROR
funcA({ type: &#39;B&#39;, opt3: &#39;Hello&#39; }) // TS ERROR: The expected type comes from property &#39;type&#39; which is declared here on type &#39;FuncAOptions&#39;

I like having the map typed with K in AllFunctions[&#39;type&#39;] because when I add a function to AllFunctions, I am reminded that I need to add the key-value pair to functionMap.

Full example here

答案1

得分: 1

如果你希望 functionMap[type](options) 在没有使用 类型断言any 类型 松散化的情况下进行类型检查,那么你需要将它写成 泛型 索引 到基本键值类型或 映射类型 的形式。这就像在 microsoft/TypeScript#47109 中描述的那样。

基本上,你希望将 type 视为某种泛型类型 K,将 functionMap 视为某种映射类型,例如 {[P in keyof FuncOptionMap]: (arg: FuncOptionMap[P]) => FuncRetMap[P]},将 options 视为类型 FuncOptionMap[K]。然后编译器可以得出结论,函数 functionMap[type] 的类型是 (arg: FuncOptionMap[K]) => FuncRetMap[K],因此可以使用 options 作为参数进行调用,并将返回相应的返回类型 FuncRetMap[K]。因此,我们需要根据你拥有的值来定义 FuncOptionMapFuncRetMap

你可能希望只使用 type 作为等同于 keyof FuncOptionMap联合类型 而无需使用泛型。但 TypeScript 无法遵循这种逻辑,正如在 microsoft/TypeScript#30581 中所述。建议的方法是改为使用泛型索引到映射类型;实际上 microsoft/TypeScript#47109microsoft/TypeScript#30581 的解决方案(或者至少是我们现在最接近的解决方案)。

代码示例如下:

const _functionMap = {
    A: funcA,
    B: funcB
}

type FuncOptionMap =
    { [K in keyof typeof _functionMap]: Parameters<typeof _functionMap[K]>[0] }
type FuncRetMap =
    { [K in keyof typeof _functionMap]: ReturnType<typeof _functionMap[K]> }

const functionMap: { [K in keyof FuncOptionMap]: (options: FuncOptionMap[K]) => FuncRetMap[K] }
    = _functionMap

基本上,我们将你的 functionMap 重命名为 _functionMap,然后稍后用从它计算得出的 FuncOptionMapFuncRetMap 类型重新赋值它。虽然 _functionMap 的类型和 functionMap 的类型似乎相同,但这实际上非常重要;只有当事情以这种方式编写时,编译器才能遵循逻辑。如果你尝试在后续使用 _functionMap 而不是 functionMap,编译器将会迷失方向并输出错误信息。

继续:

function callFunction<K extends keyof FuncOptionMap>(
    type: K, options: FuncOptionMap[K]
) {
    return functionMap[type](options); // 正常
}

这将进行类型检查,并且返回类型是 FuncRetMap[K]。现在你会得到你期望的错误:

callFunction('A', { type: 'B', opt3: 'Hello' }) // 错误!
// ---------------------~~~ B 不是 A

当你调用它时,编译器知道输出类型如何取决于输入类型:

console.log(callFunction(
    "B",
    { type: "B", opt3: 1, opt4: x => x + 1 }
).toLowerCase()); // 正常,编译器知道它是字符串而不是数字

[代码的 Playground 链接](https://www.typescriptlang.org/play?#code/C4TwDgpgBAYgrgOwMYEEDyZgEsD2CDOUAvFAN4BQUVUokAXFAOQqOXU6YCMD+wATlgQBzNlQ7AATAwBGOHABsIAQwTkAvuXIAzREmx4oO5CgAU43AQbxjGfQQCUDBHAC20iHzKioWLVBMAhOZ4+AB04pz2NAAWfDgA7lAIEIkAonxxfCaMALJY+PiCQlB8EACOcFilACZQwQiM9t6lwHB8CFCc6pq00NZIAEK2FoQkFNQ04BAMjAOsE+IAzE6u7nze4gAsDCZKK24eUUQAfEmrHt3aunaGugNmmCNWd8MhjlC8AsJeE77+QY8QuFMIsosBYgkkikoOlMtk8gUiiVypUanVAQ0mhMWm0OrNWBpyEgQsAoAB9Ix6Cw5JRgYg-agoBiUlAAGm8A2Zd0uvVguleCBpdKI3lIUAA2gBpHwdADWEBAOD8vSV5MpdiFAF0GAAFJR8JQuCDADz4AA8Kr8FOu1NpUs1x3FAAZNVANLz+gAlY1C4iiiXSwRQeWK5VTVXW5Aa2naqDe1rtAAqUwt4at6ttYHtp0JxIIpIzeCFDDFUplwYVqv6Aq1O3q+GeyBrdslmqOpy9Ppbro0ExIkapRdpmkLHSQSnk8n6djN0ogAA8

英文:

If you want functionMap[type](options) to type check without loosening things up with type assertions or the any type, then you'll need to write it terms of generic indexes into a base key-value type or mapped types of that type. This is as described in microsoft/TypeScript#47109.

Essentially you want type to be seen as some generic type K, and functionMap to be seen as some mapped type like {[P in keyof FuncOptionMap]: (arg: FuncOptionMap[P]) =&gt; FuncRetMap[P]} and for options to be seen as type FuncOptionMap[K]. Then the compiler can conclude that the function functionMap[type] is of type (arg: FuncOptionMap[K]) =&gt; FuncRetMap[K], and is therefore callable with options as an argument, and will return the corresponding return type FuncRetMap[K]. So we'll need to define FuncOptionMap and FuncRetMap in terms of the values you have.

You might have hoped that you could have done this with type merely being the union type equivalent to keyof FuncOptionMap without needing generics. But TypeScript can't follow that sort of logic, as described in microsoft/TypeScript#30581. The recommended approach is to use generic indexing into mapped types instead; indeed microsoft/TypeScript#47109 is the solution to microsoft/TypeScript#30581 (or at least the closest we have to a solution).

It could look like this:

const _functionMap = {
    A: funcA,
    B: funcB
}

type FuncOptionMap =
    { [K in keyof typeof _functionMap]: Parameters&lt;typeof _functionMap[K]&gt;[0] }
type FuncRetMap =
    { [K in keyof typeof _functionMap]: ReturnType&lt;typeof _functionMap[K]&gt; }

const functionMap: { [K in keyof FuncOptionMap]: (options: FuncOptionMap[K]) =&gt; FuncRetMap[K] }
    = _functionMap

Essentially we rename your functionMap to _functionMap and then later assign it back with the FuncOptionMap and FuncRetMap types computed from it. It might look like a no-op, since the type of _functionMap and the type of functionMap appear to be identical. But this is actually very important; the compiler can only follow the logic when things are written as this mapping. If you try to use _functionMap instead of functionMap in what follows, the compiler will lose the thread and output an error.

Continuing:

function callFunction&lt;K extends keyof FuncOptionMap&gt;(
    type: K, options: FuncOptionMap[K]
) {
    return functionMap[type](options); // okay
}

That type checks, and the return type is FuncRetMap[K]. Now you get the error you expected:

callFunction(&#39;A&#39;, { type: &#39;B&#39;, opt3: &#39;Hello&#39; }) // error!
// ---------------------&gt; ~~~ B is not A

and when you call it the compiler knows how the output type as depends on the input type:

console.log(callFunction(
    &quot;B&quot;,
    { type: &quot;B&quot;, opt3: 1, opt4: x =&gt; x + 1 }
).toLowerCase()); // okay, the compiler knows it&#39;s string and not number

Playground link to code

huangapple
  • 本文由 发表于 2023年6月1日 22:21:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76382916.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定