5

This works in Javascript and Typescript:

class A  { /* ... */ }
const B = class extends A { /* ... */ }
var x = new B();
console.log(x instanceof B, x.constructor.name); // true B

But if I try to declare the type of x as B:

var x: B = new B();

I get Typescript Error:

'B' refers to a value, but is being used as a type here. Did you mean 'typeof B'?

(Note, I also get the same error if I replace const B = class extends A { /* ... */ } with simply const B = A, which is what I had originally to make things as simple as possible, but updated on review).

I don't really understand why this should be the case. According to runtime Javascript x is a B (as shown in the console.log above). And all classes are objects ("values") under the hood anyway (the Typescript docs themselves equate classes to "constuctor objects"). I'm guessing it's just a limitation of Typescript static analysis - it can't figure out that "B" is a constructor object just like "A" is and track down its typing?

And also, I don't think so, but wondering, is there actually a way to have this work in Typescript - know B is a constructor object like A and allow the use of B as a type??


P.S. I realize there are many questions on SO relating to "Blah refers to a value, but is being used as a type here. Did you mean 'typeof Blah'?", but I couldn't find Q&A approaching it as directly as above. Apologies if I missed it.

3
  • 2
    console.log(x instanceof B, x.constructor.name === 'B'); // true true actually returns "true false" if not compiled as typescript. So "this works in both TypeScript and JavaScript" is a bit misleading. If it returns "true true" when compiled the compiler is doing something wrong. Commented May 26, 2021 at 2:55
  • This is true false in TypeScript also. The question is just in error, and that line should probably be removed. Commented May 26, 2021 at 3:03
  • 1
    @traktor Your right, oops! The code is boiled down to bear minimum from a actual useful code. There is a slightly different case where we get true, true. I will update to remove or make clearer. Commented May 26, 2021 at 3:56

1 Answer 1

16

When you write class declarations like

class A { a = 1 }

in TypeScript, you are bringing into scope two different things which are both named A. One is the value named A, a class constructor that exists at runtime. The other is the type named A, a TypeScript interface corresponding to instances of the class; such types don't exist at runtime.

Also note that a value has a type, but it is not itself a type. And the type of the A value is not A, since the class constructor is not itself an instance of the class). Instead, it has the type typeof A (expressed using TypeScript's typeof type query operator), which is similar to the type new () => A (expressed using a construct signature).

The fact that the value and type are both named A is convenient but confusing. It is convenient because it lets you use the one term A to refer to related but distinct things without requiring you to invent new terminology for each (e.g., A for the class constructor, but InstanceA for the instance type). But it is confusing because it can give the false impression that these two things are really one thing, or that the relationship between these two things is somehow inherent in the name, when it's really just incidental.


On the other hand, when you write variable declarations like

const B = class extends A { b = 2 };

you are only bringing into scope a value named B. There is no corresponding type named B. If you want such a type, you will have to declare it yourself:

type B = InstanceType<typeof B>; 

The lack of an automatic type named B isn't because the compiler can't figure out that B is a class constructor. The compiler knows exactly what B is:

// const B: typeof B

And it knows that it constructs B instances:

type WhatBConstructs = InstanceType<typeof B>;
// type WhatBConstructs = B // this "B" is not an actual type name, btw

(Here I've used the InstanceType<T> utility type to probe the type of the B constructor for its instance type.)

It's just that variable declarations name new values but not new types.

There is an existing suggestion at microsoft/TypeScript#36348 which asks that when you bind a class constructor to a variable it should also make a named type corresponding to the instance type. If this were to be implemented, then your B type would appear when you wrote const B = class extends A {...}. If you want to see this happen, you could go there and give it a 👍, but I wouldn't expect to see any changes there for the foreseeable future.


Anyway, for now, if you create both a value and a type named B manually:

const B = class extends A { b = 2 };
type B = InstanceType<typeof B>;

then things will start working for you the way you want:

var x: B = new B(); // okay

Playground link to code

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, reading. Not sure if effects but I made a typo in OP. I've replaced const B = A with const B = class extends A { /* ... */ } which gives the same error, and which is what the original code actually looked like. Apologies.
Awesome answer thank you! Defs giving my 👍 on that issue :). type B = A is definitely progress. Though, it turns out that small update I mentioned does effect things :/. I can't say type B = B (circular reference error). Also cant do type TypeOfB = B (B is a value error).
Yeah, that B type is just something IntelliSense shows; since it's a class expression the compiler shows B to be "helpful", which (imo) is even more confusing than just saying (Anonymous class) or something like InstanceType<typeof B>. You want type B = InstanceType<typeof B> in general, which will produce a type that looks like type B = B in IntelliSense but actually works. Anyway I updated my answer.

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.