如何在 TypeScript 中一般性地约束高阶函数接收的函数参数?

问题描述 投票:0回答:1

我想创建一个函数 X,它接受一组受约束的函数 Y,使得它们的参数必须是

object
的子类型。我希望从 Y 的参数推断出 Y 的返回类型。例如,如果用户传递
[(x: {a:1}) => void]
那么返回类型应该是字面意思。

当我尝试在 TypeScript 中创建 X 时,我总是遇到逆变问题。如果我说我希望 X 只接受

(input:object) => void
子类型的函数,那么
object
就被视为逆变约束,意味着只允许
object
的超类型。我想要相反的子类型被允许。

如何实现这一目标?这是我最近对这个问题所做的尝试:

type InputFunction<$I> = <$Input extends $I>(input: $Input) => void

declare const createObjectAccepingFunctions: <const $Fs extends InputFunction<object>[]>(fs: $Fs) => { items: $Fs }

const result = createObjectAccepingFunctions([
    (_: { a: 1 }) => {}, // Should pass
    (_: string) => {}, // Should fail
])

result.items
typescript typescript-generics
1个回答
0
投票

您不能将函数的参数必须是

object
的子类型的限制表示为函数类型上的直接 通用约束。正如您所指出的,函数的参数类型是逆变的(请参阅 TypeScript 中的方差、协方差、逆变、双方差和不变性之间的差异),因此像 F extends ((x: object)=>void)[]
 这样的上界约束最终看起来像 
下界 约束关于 x
 参数。由于 TypeScript 没有对下限约束的内置支持(如 
microsoft/TypeScript#14520 中的要求),因此您也不能编写 F super ((x: object)=>void)[]
。  这样的约束本身并不完全有效,因为返回类型将是逆变的。  因此,我们必须放弃简单的约束,并进行重构。有不同的可能方法。


一种是对函数参数本身的数组/

元组进行约束,然后使用映射数组类型来表示实际的函数数组。像这样:

declare const createObjectAccepingFunctions: <const A extends object[]>( fs: { [I in keyof A]: (input: A[I]) => void } ) => { items: typeof fs }
因此,当您调用 

createObjectAccepingFunctions([(a: A1)=>{}, (a: A2)=>{}, (a: A3) =>{}])

 时,TypeScript 会将 
A
 推断为 
[A1, A2, A3]
 并将其与 
object[]
 进行比较。如果成功,那么您的调用成功,否则失败,您将收到错误消息:

const result = createObjectAccepingFunctions([ (_: { a: 1 }) => { }, (_: { a: 2 }) => { }, ]) // okay result.items; // ^? [(input: { a: 1; }) => void, (input: { a: 2; }) => void] createObjectAccepingFunctions([ (_: { a: 1 }) => { }, (_: string) => { }, ]); // error!
不幸的是,在错误情况下,

A

的推理失败并回退到约束
object[]
,因此错误消息只是抱怨一切。


另一种方法是像以前一样在输入函数数组中使函数通用,但不要尝试限制为函数数组以外的任何内容。然后我们映射该数组,作为验证步骤。每个元素要么是一个参数扩展为

object

 的函数,在这种情况下,我们保留该元素,要么它不是一个,在这种情况下,我们将其替换为其他东西,希望能为用户提供一些线索,告诉用户问题是什么:

declare const createObjectAccepingFunctions: <const F extends ((x: any) => void)[]>( fs: { [I in keyof F]: F[I] extends (x: infer A) => void ? [A] extends [object] ? F[I] : never : never } ) => { items: typeof fs }
因此,对于索引为 

F

 的元素处 
I
 的每个成员,如果 
inferred 参数 A
object
 的子类型,那么我们将其保留为 
F[I]
。否则我们将其替换为 
never
,它将无法扩展。  这会给你这样的行为:

const result = createObjectAccepingFunctions([ (_: { a: 1 }) => { }, (_: { a: 2 }) => { }, ]); // okay result.items; // ^? [(input: { a: 1; }) => void, (input: { a: 2; }) => void] createObjectAccepingFunctions([ (_: { a: 1 }) => { }, (_: string) => { }, // error! //~~~~~~~~~~~~~~~~ // Type '(_: string) => void' is not assignable to type 'never' ]);
这也不是很有帮助,但至少你的错误仅限于糟糕的论点。不幸的是,TypeScript 缺少 

microsoft/TypeScript#23689 中要求的“无效类型”,允许您自定义错误消息。理想情况下,您的消息应该是“string

 不可分配给 
object
”之类的内容。  我们能想到的最接近的方法是定义一个 
Invalid<T>
 类型,我们希望不会意外地为它分配任何内容,然后我们可以传入 
T
 一些看起来像自定义错误消息的东西(如果你眯着眼睛看的话)。  也许是这样的:

interface Invalid<T> { msg: T; } declare const createObjectAccepingFunctions: <const F extends ((x: any) => void)[]>( fs: { [I in keyof F]: F[I] extends (x: infer A) => void ? [A] extends [object] ? F[I] : Invalid<[A, "is not assignable to object"]> : never } ) => { items: typeof fs }
现在我们得到了相同的成功行为,但失败看起来像

createObjectAccepingFunctions([ (_: { a: 1 }) => { }, (_: string) => { }, // error! //~~~~~~~~~~~~~~~~ // Type '(_: string) => void' is not assignable to type // 'Invalid<[string, "is not assignable to object"]>'. ]);
这已经是我能想到的最接近有意义的错误了。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.