1

I want to type a function that takes as input a variety of classes in the form of array elements or array of arrays consisting of a class and the name of its method. Something like that:

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

I also want to rigidly indicate that if [[class, method]] pattern is selected, then it is possible to specify the method of only this particular class.

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
]);

Is there some way for the type variable T to pick the type of the class on each iteration (array element)?

Playground

2
  • Can you explain what you have tried and where you are seeing issues? Commented Mar 26, 2021 at 15:48
  • I was trying to come up with an indexed construct for T. Something like T[I in keyof T], but it didn't work for me. Commented Mar 26, 2021 at 15:55

2 Answers 2

2

Referencing this thread- https://github.com/microsoft/TypeScript/issues/31617 and the following code-

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 The inferred type for property a in object in arguments in function fn is ArgType<A | B>[]. If you check type of X, it is Class<A | B> | [Class<A | B>, never]. That is, the keys have type never because intersection of keyof A and keyof B is never.

It seems it is impossible to do such thing in Typescript as of now.

Other links-

  1. TypeScript generics will only infer union types in simple cases
  2. https://github.com/microsoft/TypeScript/issues/31617#issuecomment-496867013
Sign up to request clarification or add additional context in comments.

Comments

2

Firstly, ensure the compiler your array will not be mutated, in other words, is a tuple. If you pass in an array literal, you will have to use const assertion (as const). As soon as you do that, you will see the type you defined is unsuitable for the task:

The type 'readonly [typeof A, readonly [typeof A, "a"], typeof B, readonly [typeof B, "b"]]' is 'readonly' and cannot be assigned to the mutable type '(Class | [Class, never])[]'

Note that keyof T is resolved to never because keyof unknown is never. Why? Because the signature accepts a mutable Array, while we pass a ReadonlyArray. This is easily fixed, but takes us to square one as T is inferred as the type of the first element of the tuple:

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

So, secondly, we have to stop the compiler from inferring T. How? The simplest way (assuming you do not care about the exact type of constructors passed in) is to stop passing through the instance type and be content with a non-generic 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)

All is well now, but we lost the ability to constrain the second member of the tuple to keyof of the instance (this is why the only constraint is string). Because T is now inferred as a tuple with members correctly typed, we can wrap the arg type into a mapped type to correct the shortcoming.

Something like this should do (all we do is reconstrain the second member of the tuple to keyof with a help of an InstanceType utility type):

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]
};

And voila, the resulting signature is strictly typed:

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"'

Notice that we could even drop the as const assertion, but be careful: if you do that, the passed in tuple has to be homogenous, or the second member of the tuple will not be narrowed to a string literal:

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

Playground

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.