是否可以避免这个泛型函数体中的类型断言?

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

作为一个用例,我希望允许人们向对象添加任意函数,并使用其名称和参数通过另一个函数调用该函数。

// Example arbitrary function
const sum = (a: number, b: number): number => a + b

// Another such function
const saySomething = (): string => Math.random() < 0.5 ? 'morning' : 'evening'

// This object holds the functions declared above
const execFn: Record<string, (...args: any[]) => any> = { sum, saySomething }

// These functions can be called through a generic function
export const exec = (name: string, ...args: any[]): any => execFn[name](...args)

console.log(exec('sum', 1, 2))

这可行,但它不是类型安全的。例如,有人可能输入了错误的参数。我可以使用映射类型来改进这一点,如下所示:

type GenericFn = keyof typeof execFn

type GenericParameters = {
  [K in GenericFn]: Parameters<(typeof execFn)[K]>
}

type GenericReturn = {
  [K in GenericFn]: Parameters<(typeof execFn)[K]>
}

然后修改

exec
函数签名:

// These functions can be called through a generic function
export const exec = <T extends GenericFn>(
  name: T,
  ...args: GenericParameters[T]
): GenericReturn[T] => (execFn[name] as (...args: any[]) => any)(...args)

现在,TS 将正确判断是否有人在没有正确参数的情况下调用函数,并且返回类型已正确键入。

console.log(exec('sum', 1)) // Expected 3 arguments, but got 2. ts(2554)

如何删除

exec
函数中的类型断言?

// These functions can be called through a generic function
export const exec = <T extends GenericFn>(
  name: T,
  ...args: GenericParameters[T]
): GenericReturn[T] => execFn[name](...args)

删除类型断言会引发以下 TS 错误。

Type 'string | number' is not assignable to type 'GenericReturn[T]'.
  Type 'string' is not assignable to type 'GenericReturn[T]'.
    Type 'string' is not assignable to type 'never'.
      The intersection '[a: number, b: number] & []' was reduced to 'never'
      because property 'length' has conflicting types in some constituents.ts(2322)

尝试隔离函数给我的印象是通用函数没有从对象中获取精确的函数。

const fn = execFn[name]

// const fn: {
//   sum: (a: number, b: number) => number;
//   saySomething: () => string;
// }[T]

TypeScript 游乐场

typescript generics compiler-warnings mapped-types type-assertion
1个回答
0
投票

这实际上非常接近某些处理输入输出类型依赖性的推荐方法,如 microsoft/TypeScript#47109 中所述。为了使以下代码正常工作:

export const exec = <K extends GenericFn>(
  name: K,
  ...args: GenericParameters[K]
): GenericReturn[K] => execFn[name](...args)

编译器需要将

execFn[name]
视为泛型类型
(...args: GenericParameters[K]) => GenericReturn[K]
的单个函数。这意味着它需要看到
execFn
是由
K
类型的键索引时的一般行为。

根据 microsoft/TypeScript#47109,如果

execFn
表示为 K 中的
GenericFn
键上的
映射类型
,则会发生这种情况。具体来说:

const execFn: { 
  [K in GenericFn]: (...args: GenericParameters[K]) => GenericReturn[K] 
} = ⋯;

由于您的

GenericParameters
GenericReturn
类型本身就是用
typeof execFn
编写的,因此我们无法在没有循环的情况下直接这样做。让我们将您的
execFn
重命名为
_execFn
:

const _execFn = {
  isEven, randomInt, repeat, saySomething, sum,
} as const;

type GenericFn = keyof typeof _execFn

type GenericParameters = {
  [K in GenericFn]: Parameters<(typeof _execFn)[K]>
}

type GenericReturn = {
  [K in GenericFn]: ReturnType<(typeof _execFn)[K]>
}

现在我们可以将

_execFn
分配给适当类型的
execFn

const execFn: {
  [K in GenericFn]: (...args: GenericParameters[K]) => GenericReturn[K]
} = _execFn;

这有效。


请注意,这取决于 execFn 类型的

form
。如果您通过 IntelliSense 查看
execFn
的类型:

/* const execFn: {
    isEven: (n: number) => boolean;
    randomInt: (upTo: number) => number;
    repeat: (word: string, times: number) => string[];
    saySomething: () => string;
    sum: (a: number, b: number) => number;
} */

并将其与显示的类型进行比较

_execFn
:

/* const _execFn: {
    readonly isEven: (n: number) => boolean;
    readonly randomInt: (upTo: number) => number;
    readonly repeat: (word: string, times: number) => string[];
    readonly saySomething: () => string;
    readonly sum: (a: number, b: number) => number;
} */

这些基本上是相同的(

readonly
在这里并不重要)。当然,它们必须如此,否则
const execFn: ⋯ = _execFn
作业将会失败。

但是类型的内部表示是不同的。

execFn
的类型明确是映射类型,其中每个属性都具有通用关系,而
_execFn
的类型只是可能不相关的属性的列表。编译器缺乏查看
_execFn
并自行得出
execFn
类型的能力;需要明确告知。

所以

execFn[name]
的类型是一个接受
GenericParameters[K]
类型的参数列表的单一函数,这很好。但是
_execFn[name]
的类型最终会扩展为函数的 union ,这是非常无用的,因为函数的并集只能通过参数的 intersection 来安全地调用(参见 TS3.3 发行说明) 。因此
_execFn[name]
想要一个
never
类型的参数列表(即,没有安全的方法来调用这个函数联合,因为没有参数适用于每个调用签名)。

事实上,最终导致 microsoft/TypeScript#47109 方法的问题是关于这些无用的函数联合,如 microsoft/TypeScript#30581 中所述。这个问题用“相关联合”来表述,其中您有一个像

v
这样的联合类型的值
{k: "sum", fn: (a: number, b: number) => number, args: [a: number, b: number]} | {k: "saySomething", fn: () => string, args: []} | ⋯}
并且您想调用
v.fn(...v.args)
,但编译器不允许您这样做。通用索引是解决方案的一部分,但您还需要映射类型。

Playground 代码链接

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