typescript TS泛型和派生类型

7xllpg7q  于 2023-05-01  发布在  TypeScript
关注(0)|答案(1)|浏览(210)

我想创建一个函数来处理调用其他函数。这些其他函数应该触发类似的业务和清理逻辑。

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)就可以了吗?

h79rfbju

h79rfbju1#

正如您所看到的,编译器无法看到FUNCTIONS[type]类型和arg类型之间的相关性。这个问题本质上与microsoft/TypeScript#30581中描述的相同,在microsoft/TypeScript#30581中,它是按照相关的联合类型来表述的。除非你非常小心,否则当编译器试图调用FUNCTIONS[type](arg)时,它会将FUNCTIONS[type]generic类型扩展为并集((arg: string) => void) | (arg: number) => void)),而arg类型扩展为string | number,不幸的是,你不能用后者的参数调用前者。实际上,编译器将函数的并集折叠成其参数的交集的单个函数(如TS 3的发行说明中所述)。3)你得到了(arg: never) => void,它根本不能被调用,因为它需要一个不可能的never类型的参数;不能同时是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中所述。其思想是重构您的类型,以便将它们显式地作为泛型indexed accesses上的操作写入mapped types,而不是简单或“基本”对象类型。关键是,您希望fn被视为具有泛型类型(arg: XXX) => void,并且您希望arg被视为相同泛型XXX的类型XXX。因此FUNCTIONS的类型必须更改为Map到该基类型。
这里有一个方法。首先,我们将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;
} */

然后我们声明FUNCTIONSFnArg上的Map类型:

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;
} */

它们之间的唯一区别是它们在内部的表示方式,Map类型FUNCTIONS允许编译器看到FUNCTIONS[type]arg之间的相关性。如果你在callFuction()的主体中用_FUNCTIONS替换FUNCTIONS,你原来的错误会马上回来。
因此,这个问题是一个微妙的问题,我在这里所能做的就是向人们指出Microsoft/TypeScript#47109,以获得这项技术的源代码,在那里实现者解释它是如何工作的以及为什么工作的。
Playground链接到代码

相关问题