英文:
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: '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'
}
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['type']]: (options: any) => 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('A', { type: 'B', opt3: 'Hello' }) // NO TS ERROR
funcA({ type: 'B', opt3: 'Hello' }) // TS ERROR: The expected type comes from property 'type' which is declared here on type 'FuncAOptions'
I like having the map typed with K in AllFunctions['type']
because when I add a function to AllFunctions
, I am reminded that I need to add the key-value pair to functionMap
.
答案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]
。因此,我们需要根据你拥有的值来定义 FuncOptionMap
和 FuncRetMap
。
你可能希望只使用 type
作为等同于 keyof FuncOptionMap
的 联合类型 而无需使用泛型。但 TypeScript 无法遵循这种逻辑,正如在 microsoft/TypeScript#30581 中所述。建议的方法是改为使用泛型索引到映射类型;实际上 microsoft/TypeScript#47109 是 microsoft/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
,然后稍后用从它计算得出的 FuncOptionMap
和 FuncRetMap
类型重新赋值它。虽然 _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()); // 正常,编译器知道它是字符串而不是数字
英文:
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]) => 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]) => 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<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
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<K extends keyof FuncOptionMap>(
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('A', { type: 'B', opt3: 'Hello' }) // error!
// ---------------------> ~~~ 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(
"B",
{ type: "B", opt3: 1, opt4: x => x + 1 }
).toLowerCase()); // okay, the compiler knows it's string and not number
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论