Aviad Hadad
Aviad Hadad

Reputation: 1717

Infering type of an abstract class

I have a bit of a convoluted case, I'm 99% sure it's impossible to solve in typescript, and the issue very clearly comes from connecting Typescript code with unsafe JS code. But I've seen miracles preformed on this site before...

I have some JS code, which I can't control but I must use, that takes an automatically generated abstract class (from protobuf definition, but it doesn't matter) extends and creates it. The automatically generated classes have d.ts files attached, which is the important part, as I want to keep their type information for autocomplete purposes.

So, let's start by wrapping the JS code that extends and creates the abstract classes ->

type getCtorType<Ctor> = Ctor extends { new(): infer T } ? T : never;

const initalizeClass = <Ctor extends { new(): any }>(abstractCtor: Ctor): getCtorType<Ctor> => {
    return null as any; //In reality, here there is a call to javascript code that does all the magic
}

so let's say I have two classes for example sake, one is abstract the other isn't:

class Foo {
    someFunc(a: string) {}
}

abstract class Bar {
    someFunc(a: string) { };
}

and we initalize them:

//This has type inference;
const instance1 = initalizeClass(Foo);
//This is an error
const instance2 = initalizeClass(Bar);

The second line fails, because an abstract class does not satisfy the constraint of { new (): any} as it cannot be created.

If I remove this constraint from initalizeClass and use something like Function instead,

const initalizeClassAlternative = (abstractCtor: Function) => {
    return null as any; //In reality, here there is a call to javascript code that does all the magic
}

If I do this, I lose all type inference.

Can anyone think of a way/trick to extract the type out of an abstract class passed to a function? in a similar way to how it's possible to extract the type of a regular class?


If you are wondering why something like this isn't good enough and I really want type inference:

const instance2: Bar = initalizeClassAlternative(Bar);

Here is a playground link. The example there is closer to my real use case, since this question is already too long I cut it out... But in short I'm trying to use mapped types.

Upvotes: 0

Views: 306

Answers (1)

jcalz
jcalz

Reputation: 329198

Before there was type inference in conditional types, there were lookup types. According to the standard library, the Function interface has a prototype property of type any. Classes (even abstract classes) will override this property with a type corresponding to the instance type of the class in question. So, as a quick workaround for your issue, you can examine this property:

type InstanceType<Ctor> = Ctor extends Function ? Ctor['prototype'] : never; 

(I changed the name from getCtorType<> to InstanceType<> for aesthetic reasons you may not care about, so feel free to change it back.) This lets you use your second approach:

declare const initalizeClass: <Ctor extends Function>(
    abstractCtor: Ctor
) => InstanceType<Ctor>;

abstract class Bar {
    someFunc(a: string) { };
}

const bar = initalizeClass(Bar);
bar.someFunc("works");

Looks good to me! There are further possible refinements if you want to exclude passing non-class-like Function types to InstanceType (e.g., require that the prototype property not be any), but they are probably not necessary.

Hope that helps; good luck!

Upvotes: 1

Related Questions