我想创建一个负责调用其他一些函数的函数.这些其他功能应该会触发类似的业务和清理逻辑.

function foo(arg: number) {
    // do something with arg
}

function bar(arg: string) {
    // do something with arg
}

const FUNCTIONS = {
    foo,
    bar,
} as const;

type FnType = keyof typeof FUNCTIONS;

type FnArg<Type extends FnType> = Parameters<typeof FUNCTIONS[Type]>[0];

function callFunction<Type extends FnType>(type: Type, arg: FnArg<Type>) {
    // some business logic here that is shared by all functions

    const fn = FUNCTIONS[type];

    // the type of fn and arg are both derived from Type:
    // - 'typeof fn' would be 'typeof Functions[Type]'
    // - 'typeof args' would be 'FnArg<Type>'
    // However, TS seems to see these 2 types as independent and cannot
    // figure out that fn and arg can work together.

    return fn(arg); // -> TS doesn't 'know' that arg should have the right type although we know that's the case thanks to generics
}

// generics make sure the second argument is of the right type
callFunction('foo', 5);
callFunction('foo', 'arg'); // errors as expected

callFunction('bar', 'arg');
callFunction('bar', 5); // erros as expected

我能够使用泛型来确保TS判断将被代理到这些函数的参数的类型是否正确.然而,在函数实现中,TS似乎不知道泛型是否会确保参数的类型正确.

你知道有没有办法让TS明白拨打fn(arg)就可以了?

推荐答案

正如您所看到的,编译器看不到FUNCTIONS[type]类型和arg类型之间的相关性.这个问题基本上与第microsoft/TypeScript#30581节中描述的相同,在第microsoft/TypeScript#30581节中,它的措辞是相关的union types.除非您非常小心,否则当编译器try 调用FUNCTIONS[type](arg)时,它会将FUNCTIONS[type]generic类型扩大到联合((arg: string) => void) | (arg: number) => void)),并将arg的类型扩大到string | number,不幸的是,您不能使用后者的参数来调用前者.事实上,编译器将函数的并集折叠为intersection个参数的单个函数(如the release notes for TS3.3中所述),得到(arg: never) => void,这是根本不能调用的,因为它需要一个the impossible never type的参数;不能同时是stringnumber.因此,如果你这样做,你就会被卡住:

function callFunction<K extends keyof FnArg>(type: K, arg: FnArg[K]) {
  const fn = FUNCTIONS[type];
  // const fn: (arg: never) => void
  // (parameter) arg: string | number
  return fn(arg); // error! ?
}

幸运的是,有一种方法可以修复它,如第microsoft/TypeScript#47109节所述.我们的 idea 是重构您的类型,以便将它们显式地编写为简单或"基本"对象类型上的泛型indexed accessesmapped types上的操作.关键是,您希望fn被视为具有泛型类型(arg: XXX) => void,并且希望arg被视为同一泛型XXX的类型XXX.因此,必须更改FUNCTIONS的类型以映射到该基本类型上.

有一种方法可以做到这点.首先,我们为您的FUNCTIONS变量重新命名:

const _FUNCTIONS = {
  foo,
  bar,
} as const;

然后,我们使用它来构造基本对象类型:

type FnArg = { [K in keyof typeof _FUNCTIONS]:
  Parameters<typeof _FUNCTIONS[K]>[0] }

/* type FnArg = {
    readonly foo: number;
    readonly bar: string;
} */

然后我们将FUNCTIONS声明为FnArg上的映射类型:

const FUNCTIONS: {
  [K in keyof FnArg]: (arg: FnArg[K]) => void
} = _FUNCTIONS;

现在,您可以编写callFunction()作为对通用索引访问的操作:

function callFunction<K extends keyof FnArg>(type: K, arg: FnArg[K]) {
  const fn = FUNCTIONS[type];
  // const fn: (arg: FnArg[K]) => void
  // (parameter) arg: FnArg[K]
  return fn(arg); // okay
}

这就对了!


请注意,FUNCTIONS的类型和_FUNCTIONS的类型是等价的,特别是当您要求IntelliSense显示它们时:

/* const _FUNCTIONS: {
    readonly foo: (arg: number) => void;
    readonly bar: (arg: string) => void;
} */

/* const FUNCTIONS: {
    readonly foo: (arg: number) => void;
    readonly bar: (arg: string) => void;
} */

它们之间的唯一区别是它们在内部表示的方式,而映射类型FUNCTIONS类型允许编译器看到FUNCTIONS[type]arg之间的相关性.如果在callFuction()的正文中将FUNCTIONS替换为_FUNCTIONS,则会立即返回原始错误.

因此,这个问题是一个微妙的问题,我在这里所能做的就是让人们参考Microsoft/TypeScrip#47109获取该技术的源代码,在那里实现者解释了它是如何工作的以及为什么工作.

Playground link to code

Typescript相关问答推荐

使用类和子类以及迭代类属约束对类属递数据 struct 建模

为什么在TypScript中写入typeof someArray[number]有效?

动态调用类型化函数

为什么tsx不判断名称中有"—"的属性?'

根据另一个属性的值来推断属性的类型

在TypeScrip的数组中判断模式类型

如何在处理数组时缩小几个派生类实例之间的类型范围?

如何在react组件中使用doctype和html标签?

CDK从可选的Lambda角色属性承担角色/复合主体中没有非空断言

接口重载方法顺序重要吗?

使用TypeScrip5强制使用方法参数类型的实验方法修饰符

正确使用相交类型的打字集

Angular 16将独立组件作为对话框加载,而不进行布线或预加载

如何从上传文件中删除预览文件图标?

如何使用angular15取消选中位于其他组件中的复选框

如何从抽象类的静态方法创建子类的实例?

基于区分的联合键的回调参数缩小

可以用任意类型实例化,该类型可能与

Next 13 + React 18 createContext 类型的构建问题

TS 排除带点符号的键