Class 方法 return 基于参数类型的类型

Class method return type based on type of parameter

我有一个接受任何东西作为参数的函数,但是如果它接收到一个 function(带原型)它应该 return 它的一个实例 函数(或 class,因为它们是函数)。 如果不是,它可能看起来在 Map 和 return 中分配给该键的任何东西。

超级简化的代码看起来像 (playground):

// Go check service.get() method
const isConstructable = (fn: any) => typeof fn === 'function' && 'prototype' in fn;

class Animal {
  name: string | undefined
  constructor(name?: string){
    this.name = name;
  }
}

const service = new (class Service {
  map = new Map()

  set(key: any, value: any = key) {
    return this.map.set(key, value)
  }
  
  get<T>(key: { new (): T }): T {
    const ToResolve = this.map.get(key);
    if (isConstructable(ToResolve)) {
        return new ToResolve();
    }
    
    return ToResolve;
  }
})

service.set('bob', new Animal('Bob'))
service.set(Animal);

const genericAnimal = service.get(Animal)
const bob = service.get('bob') // Argument of type 'string' is not assignable to parameter of type 'new () => unknown'

console.log(genericAnimal instanceof Animal)
console.log(bob instanceof Animal)

我正在阅读 Using Class Types in Generics 并找到了这段代码

function create<T>(c: { new (): T }): T {
  return new c();
}

效果很好,但我需要 get(key) 不仅接受构造函数,还接受 Map 接受的任何原语,例如:

const jane = {} // object reference, since TS won't let me use symbols
service.set(jane, class Jane extends Animal {
  constructor() {
    super('Jane')
  }
})
console.log(service.get(jane).name === 'Jane')

这样我就得到了像Argument of type 'string' is not assignable to parameter of type 'new () => unknown'

这样的编译错误

还尝试了:get<T>(key: T): T extends Function ? T : unknown,这显然不起作用,但感觉很接近:(

我是否必须为 key 参数创建特定类型?定义重载?怎么样?

提前致谢

我认为您对 get 的签名非常接近。如果要使用条件类型,可以这样写:

  get<T>(key: T): T extends new () => infer I ? I : unknown;
  get(key: any) {
    const ToResolve = this.map.get(key);
    if (isConstructable(ToResolve)) {
      return new ToResolve();
    }
    return ToResolve;
  }

在这种情况下,如果类型 T 对应于 zero-arg 构造函数,则 return 类型将是该构造函数的实例类型。否则 return 类型是 unknown。请注意,编译器无法验证实现是否对应于该调用签名(请参阅 microsoft/TypeScript#33912 for more info), so I'm using a single-call signature overload,其实现类型更松散。这编译得很好,但需要注意的是,您有责任确保实现符合调用签名。


这对您的第一组示例用途来说是理想的:

const genericAnimal = service.get(Animal) // Animal
const bob = service.get('bob') // unknown

当然,set() 方法的类型非常松散,无法保证它会被正确使用。

service.set(Animal, 1234); // no error? oops.

我建议更改 set() 方法以反映 get() 方法中发生的事情:

  set<T>(
    key: T, 
    ...args: T extends new () => infer I ? [value?: I] : 
      [value: unknown]
  ): void;
  set(key: any, value?: any) {
    this.map.set(key, typeof value === "undefined" ? key : value);
  }

那个签名可能看起来很可怕,但基本上它是说如果 key 是一个构造函数那么你可以调用 set() 带有零个或一个附加参数(一个参数元组type [value?: I] 其中 I 是 class 实例类型),但如果 key 不是构造函数,则必须使用附加参数调用 set()[value: unknown].

类型的参数元组

然后如果有人试图用构造函数传递一些奇怪的东西,你会得到编译器错误:

service.set(Animal, 1234); // error!
service.set(Animal); // okay
service.set('bob', new Animal('Bob')) // okay

最后,我真的不确定如何跟踪为 non-constructor 键输入的值的类型。编译器将它们视为 unknown,这意味着如果您设置并获取它,您可能会丢失信息:

service.set("string", "hello");
console.log(service.get("string").toUpperCase()); // error at compile time
// but HELLO at runtime

你要么需要使用 type assertions or a type guard 来处理:

const str = service.get("string") as string; // I'm telling the compiler it's a string
console.log(str.toUpperCase()); // works now

const str2 = service.get("string");
console.log(
  typeof str2 === "string" ? str2.toUpperCase() : "NOT A STRING"
); // I'm testing

可能还有其他方法可以使用 assertion functions or a 来跟踪类型,但这似乎超出了问题的范围。


Playground link to code