Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typing is clumsy when instancing using class objects derived from an abstract class #9302

Closed
nahuel opened this issue Jun 21, 2016 · 10 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@nahuel
Copy link

nahuel commented Jun 21, 2016

Suppose you have an abstract class A and two concrete clases B, C derived from A, and you want to store B and C as first-class values to make instances from these values:

abstract class A  { a: any}
class B extends A { b: any}
class C extends A { c: any}

let classes  = [B, C]            // inferred type = (typeof B | typeof C)[]
let instance = new classes[1]()  // inferred type = (B | C)
instance.a                       // ok, because .a is in B and C

It will be nice if the previous example inferred the instance type as A instead of (B | C). But it works.
The problem comes when TS can't infer the classes type, like when using a Map instead of an array:

let classes2 = new Map([["b", B], ["c", C]]) // error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage.
let classes3 = new Map<string, typeof A>([["b",B], ["c", C]])
let classes4 = new Map<string, typeof B | typeof C>([["b",B], ["c", C]])

var instance3 = new (classes3.get("c"))()   // TS2511: Cannot create an instance of the abstract class 'A'.
var instance4 = new (classes4.get("c"))()   // inferred type = (B | C)

classes2 doesn't work because TS can't infer the type.
classes3 doesn't work because you can't use it for instancing.
classes4 example worked but is very clumsy for a large number of classes, you need to type (typeof B | typeof C | ...) for all the classes involved. Maybe this is acceptable but I think there is a need to be able of defining (and inferring) a type like "all concrete classes derived from A".

Below is the same example but using a concrete base class. Note in this case you can use <typeof A> as the type of the array / map values:

class A  {a : any}    // now concrete
class B extends A { b: any}
class C extends A { c: any}

let classes0 = [B, C]  // inferred type = (typeof B | typeof C)[]
let classes1 = <typeof A[]>[B,C]

let instance1 = new classes0[1]()  // inferred type = (B | C)
instance1.a                        // ok, because .a is in B and C
let instance2 = new classes1[1]()  // inferred type = A
instance2.a                        // ok, because .a is in A

let classes2 = new Map([["b", B], ["c", C]]) // error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage.
let classes3 = new Map<string, typeof A>([["b",B], ["c", C]])
let classes4 = new Map<string, typeof B | typeof C>([["b",B], ["c", C]])

var instance3 = new (classes3.get("c"))()   // inferred type = A
var instance4 = new (classes4.get("c"))()   // inferred type = (B | C)

TypeScript Version:

1.9.0-dev.20160616-1.0

@RyanCavanaugh
Copy link
Member

I think what you want is this?

let classes5 = new Map<string, new() => A>([["b",B], ["c", C]]); // OK

var instance5 = new (classes5.get("c"))(); // OK, instance5: A

@nahuel
Copy link
Author

nahuel commented Jun 22, 2016

@RyanCavanaugh thanks, your example solves this problem! Just a detail, there is a way to reference the A constructor signature to avoid repeating it on the Map one? I mean:

abstract class A  { a: any
                    constructor (x: number, y: number) {}}
class B extends A { b: any}
class C extends A { c: any}

let classes5 = new Map<string, new(x : number, y : number) => A>([["b",B], ["c", C]]); // OK
     // must repeat args signature ^^^^^^^^^^^^^^^^^^^^^^
var instance5 = new (classes5.get("c"))(1,2); // OK, instance5: A

// Is possible to reference the A constructor signature? something like this:
let classes6 = new Map<string, typeof A.new>([["b",B], ["c", C]]); 

Thanks for your reply.

@RyanCavanaugh
Copy link
Member

Unfortunately there isn't

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Jun 22, 2016
@aluanhaddad
Copy link
Contributor

It's interesting to note that using an object literal might be more typesafe and refactoring friendly than using a Map.

abstract class A  { a: any}
class B extends A { b: any}
class C extends A { c: any}

let classes  = { B, C };
let instance = new classes.B()  // inferred type = B
instance.a                       // ok, because .a is in B

@nahuel
Copy link
Author

nahuel commented Jun 25, 2016

@aluanhaddad that works, but not when you use an string based key (eg. one taken from a config file). In your example var a = 'B' ; classes[a] will be inferred as any.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 25, 2016

The keys are strings, I'm simply using concise object literal notation for brevity and in order to leverage refactoring. It's true that if you're using an index signature with a string literal, it becomes more difficult, but I don't see how it is any worse than an array, and at least it makes any necessary casting more intuitive. But you could add an index signature like

[key: string]: B | C;

or

[key: string]: new () => A;

Then again that kind of defeats the thing you were trying to achieve in the first place. When the keys are not constants, it's easy to run into this sort of thing.
Would it be possible for the config file to be a TypeScript file containing exported constants? I found that to be quite useful in certain cases. Also it looks like there will be support for typed JSON Imports at some point in the future.

@nahuel
Copy link
Author

nahuel commented Jun 25, 2016

@aluanhaddad: let me check if I get you, given this:

let classes1 = new Map<string, new() => A>([["B", B], ["C", C]]); 
let classes2 : {[k : string] : new() => A } = { B, C };

classes2 is more concise, and his initialization is more refactorizable when you change B or C names. But there are no other advantages over classes1 in the later usage when you need to access it using string keys.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 25, 2016

Actually, I think this works better,
(TypeScript 1.9.0-dev.20160624-1.0)

let classes = { B, C };

let CPrime = classes["C"]; // CPrime has the same type as C
let instance = new C(); // instance is an instance of C
console.log(instance.c);

The problem is the config file. The constant value does not seem to propagate across module boundaries. Forgot my idea of about using a .ts configuration file. It only seems to work within one file.

I had a thought that you could do

import { classKey } from "./config"; // classKey is defined as "export const classKey = 'C';"
let CPrime = classes[classKey]; // Unfortunately has type any

Edit: It's not related to modules, it has to do with string literal type inference. Even if I give a variable an explicit string literal type, it does not flow from the variable to the index signature. There are a lot of issues related to this.

@nahuel
Copy link
Author

nahuel commented Jun 25, 2016

@aluanhaddad when I said "a config file" I meant an arbitrary JSON document loaded at runtime, not an imported "config" module. But yes, I think you are pointing to another valid issue.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 26, 2016

@nahuel that probably should have been obvious to me because otherwise they wouldn't have been much of a point to the scenario you described. Sorry if I got the issue off track. I don't see how TypeScript could provide this however, without compile time constants. A run-time scenario like you mention, where the type is decided based on dynamically imported configuration is going to have to do type assertions. There is an issue tracking type providers which may be closer to what you're looking for #3136

@mhegazy mhegazy closed this as completed Sep 20, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants