基于动态、变量或索引的泛型类型变量

时间:2021-03-26 15:40:46

标签: typescript typescript-generics

我想键入一个函数,该函数以数组元素或由 classes 及其名称 class 组成的数组的形式将各种 method 作为输入。类似的东西:

fn([Class1, Class2, [Class3, 'method3'], ..., ClassN]);

我还想明确指出,如果选择了 [[class, method]] 模式,则可以仅指定该特定类的方法。

type Class<T> = new (...args: any[]) => T;

class A {
  a(): any {}
}

class B {
  b(): any {}
}

// ...

function fn<T>(arg: Array<
  Class<T> // <-- pick type of T dynamically??
  | [Class<T>, keyof T] // <-- pick type of T dynamically??
>): any {
  // do something with arg..
}

fn([
  A,        // ok
  [A, 'a'], // ok
  B,        // type error: Property 'a' is missing in type 'B' but required in type 'A'.
  [B, 'b'], // same type error..., expecting only "b" available here
]);

有没有办法让类型变量 T 在每次迭代(数组元素)时选择 class 的类型?

Playground

2 个答案:

答案 0 :(得分:2)

引用此线程-https://github.com/microsoft/TypeScript/issues/31617 和以下代码-

type Class<T> = new (...args: any[]) => T;

class A {
  a(): any {}
}

class B {
  b(): any {}
}

type ArgType<T> = Class<T> | [Class<T>, keyof T]

type ArgDefinition<T> = {
  [K in keyof T]: ArgType<T[K]>[]
}

declare function fn<T>(arg: ArgDefinition<T>): void

fn({a: [
  A,
  [A, 'a'], // shows error
  B,
  [B, 'b'], // shows error
]})

type X = ArgType<A | B>

Playground 函数 a 中参数对象中属性 fn 的推断类型为 ArgType<A | B>[]。如果您检查 X 的类型,它是 Class<A | B> | [Class<A | B>, never]。也就是说,键的类型为 never,因为 keyof Akeyof B 的交集是 never

目前在 Typescript 中似乎不可能做这样的事情。

其他链接-

  1. TypeScript generics will only infer union types in simple cases
  2. https://github.com/microsoft/TypeScript/issues/31617#issuecomment-496867013

答案 1 :(得分:2)

首先,确保编译器不会改变你的数组,换句话说,是一个元组。如果传入数组字面量,则必须使用 const 断言 (as const)。一旦你这样做,你会看到你定义的类型不适合这个任务:

<块引用>

类型 'readonly [typeof A, readonly [typeof A, "a"], typeof B, readonly [typeof B, "b"]]' 是 'readonly' 并且不能分配给可变类型 '(Class | [Class, never])[]'

请注意,keyof T 被解析为 never,因为 keyof unknownnever。为什么?因为签名接受一个可变的 Array,而我们传递一个 ReadonlyArray。这很容易解决,但需要我们求平方,因为 T 被推断为元组的第一个元素的类型:

function fn<T>(arg: ReadonlyArray<
  Class<T> // <-- pick type of T dynamically??
  | [Class<T>, keyof T] // <-- pick type of T dynamically??
>): any {
  // do something with arg..
}

fn([A] as const); //function fn<A>(arg: readonly (Class<A> | [Class<A>, "a"])[]): any

因此,其次,我们必须阻止编译器推断 T。如何?最简单的方法(假设您不关心传入的构造函数的确切类型)是停止传递实例类型并满足于非泛型 Ctor

type Ctor = new (...args: any[]) => any;

function fn2<T extends readonly (Ctor | readonly [Ctor, string])[]>(arg: T): any {
  // do something with arg..
}

fn2([
  A,        // ok
  [A, 'a'], // ok
  B,        // ok
  [B, 'b'], // ok
] as const)

现在一切都很好,但我们失去了将元组的第二个成员约束到实例的 keyof 的能力(这就是为什么唯一的约束是 string)。由于 T 现在被推断为成员类型正确的元组,我们可以将 arg 类型包装成映射类型以纠正缺点。

应该这样做(我们所做的就是借助 keyof utility type 将元组的第二个成员重新约束为 InstanceType):

type Constrained<T extends readonly (Ctor | readonly [Ctor, string])[]> = {
    [P in keyof T] : T[P] extends readonly any[] ? readonly [ T[P][0],  keyof InstanceType<T[P][0]> ] : T[P]
};

瞧,生成的签名是严格输入的:

fn2([
  A,        // ok
  [A, 'a'], // ok
  B,        // ok
  [B, 'b'], // ok
  [B, "missing"] // Type '"missing"' is not assignable to type 'keyof B'
] as const)

fn2([[A, "a"]]) //ok
fn2([[A, "unknown"]]) //Type '"unknown"' is not assignable to type '"a"'

请注意,我们甚至可以删除 as const 断言,但要小心:如果这样做,传入的元组必须是同构的,否则元组的第二个成员将不会缩小为字符串文字:

fn2([[A, "never"], B]) //ok as the inferred type is ([typeof A, string] | typeof B)[]

Playground