Typescript using mapped types with arrays

Viewed 123

I'm trying to use mapped types with typescript, which is supposed to be possible, but running into some issues.

Let's say I have 3 classes:

class A {
    public foo1 = 1;
}
class B {
    public foo2 = 1;
}
class C {
    public foo3 = 1;
}

const base = [A, B, C] as const;

Basically, what I want is to pass base into some function, and get back an array with 3 matching instances.

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

function instantiate1<T extends ReadonlyArray<CTOR>>(arr: T): {[K in keyof T]: T[K]} {
    const items = arr.map(ctor => new ctor());
    return items as any; //Don't mind about breaking type safety inside the function
}

Now this returns:

const result1 = instantiate2(base) //type is [typeof A, typeof B, typeof C]

which makes sense, as I didn't really "map" the type yet, just made sure the syntax works.

However if I try to actually map the type:

function instantiate2<T extends ReadonlyArray<CTOR>>(arr: T): {[K in keyof T]: InstanceType<T[K]>} {
    const items = arr.map(ctor => new ctor());
    return items as any; //Don't mind about breaking type safety inside the function
}

I get an error that T[K] doesn't satisfy the constraint of instance type.

anyone as an idea for a workaround?

full playground link

1 Answers

This is a known bug in TypeScript, according to microsoft/Typescript#27995. It's a fairly old issue now and is on the "Backlog", so I wouldn't expect it to be fixed anytime soon.


Workarounds:

Explicitly constrain T[K] to the desired constructor type. I usually do this with the Extract<T, U> utility type like this:

declare function instantiate<T extends ReadonlyArray<Ctor>>(arr: T): {
    [K in keyof T]: InstanceType<Extract<T[K], Ctor>>
};

const result = instantiate(base);
// const result: readonly [A, B, C]

Or, you could replace the InstanceType<T> utility type with your own version that doesn't care if T is a constructor. The existing type definition is:

type InstanceType<T extends new (...args: any) => any> = 
   T extends new (...args: any) => infer R ? R : any;

So you can loosen it to:

type MyInstanceType<T> = // no constraint on T
    T extends new (...args: any) => infer R ? R : any;

and then your original version works as expected:

declare function instantiate<T extends ReadonlyArray<Ctor>>(arr: T): {
    [K in keyof T]: MyInstanceType<T[K]>
};

const result = instantiate(base);
// const result: readonly [A, B, C]

Playground link to code

Related