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
T. Something likeT[I in keyof T], but it didn't work for me.