从映射解析并间接调用的函数的类型推断

Type inference of function resolved from a map and called indirectly

我正在尝试使打字稿类型与一种模式一起工作,其中有一个 对象 函数和一个函数 call(name, arg)object by name 并用 arg.

调用它

假设我有一个将名称映射到函数的对象:

interface Registry {
  str2numb: (p: string) => number
  num2bool: (p: number) => boolean
}

const REGISTRY: Registry = {
  str2numb: p => parseInt(p, 10),
  num2bool: p => !!p,
}

我还有一个函数 call(name, p),它从 REGISTRY 解析函数并用 p 调用它。现在,我想输入函数,如果提供了无效参数,它会抱怨:

const call = (name, p) => REGISTRY[name](p)

call('str2numb', 123)
//               ^^^ Would like to see an error here

如何从 Registry.str2numb 的类型解析参数 p 的类型 P(以及 return 类型 R)?有可能吗?

// How can I resolve P and R here?
// The resolved function is Registry[N]
// I have tried Registry[N]<P, R> but that doesn't work :-(
const call = <N extends keyof Registry>(name: N, p: P): R => REGISTRY[name](p)

我已经走到这一步了,但还是不行:

type Fn<P, R> = (p: P) => R

const call =
  <N extends keyof Funcs, F extends Funcs[N] & Fn<P, R>, P, R>
    (name: N, p: P): R =>
      REGISTRY[name](p)

call('str2numb', 123)
//               ^^^ No error here

然而这有效:

// This just returns the resolved function
const call1 = <N extends keyof Funcs>(name: N) => REGISTRY[name]

// The type of the returned function is correctly inferred from the name
call1('str2numb')(123)
//                ^^^ Argument of type '123' is not assignable to parameter of type 'string'

无法从打字稿中的函数类型'extract out'参数类型。

如果您愿意做一些额外的工作,您可以使用单独编码参数类型和 return 类型的数据结构为注册表定义类型。该数据结构不在运行时使用,仅用作编译器进行类型推断的指南,因此您可以对 call 进行类型检查:

// used to encode parameter and result type
// like this: param<string>().result<number>()
function param<P>(): { result<R>(): {p: P[], r: R[]}} {
    return {
        result<R>() {
            return {p: [], r: []} // use empty arrays so we don't have 
                                  // to provide values
        }
    }
}

const registryTypes = {
    str2numb: param<string>().result<number>(),
    num2bool: param<number>().result<boolean>()
}
type RegistryTypes = typeof registryTypes;

// this has the same type as `interface Registry` in the question
type Registry = {[N in keyof RegistryTypes]: (p: RegistryTypes[N]['p'][0]) => RegistryTypes[N]['r'][0]};


const REGISTRY: Registry = {
  str2numb: p => parseInt(p, 10),
  num2bool: p => !!p,
}

let call: <N extends keyof RegistryTypes>(n: N, p: RegistryTypes[N]['p'][0]) => RegistryTypes[N]['r'][0];

const n = call('str2numb', '2'); // ok, n is a number
const n1 = call('str2numb', 2); // error

我基本上同意@artem,为了完整性,我发布了这个相似但不相同的解决方案:

// type for the compiler
type RegistrySchema = {
  str2numb: { argument: string, result: number };
  num2bool: { argument: number, result: boolean };
}

// represent Registry in terms of RegistrySchema
type Registry = {
  [K in keyof RegistrySchema]:
    (argument: RegistrySchema[K]['argument']) => RegistrySchema[K]['result'] 
}

// same REGISTRY as before
const REGISTRY: Registry = {
  str2numb: p => parseInt(p, 10),
  num2bool: p => !!p,
}

// call can be defined thusly
function call<K extends keyof RegistrySchema>(
  k: K,
  argument: RegistrySchema[K]['argument']
): RegistrySchema[K]['result'] {
  return REGISTRY[k](argument);
}

// it works
const x = call('str2numb', 123); // error
const y = call('str2numb', "hello"); // y is number

祝你好运!