0

I have the following code:

class Hey {
  static a: string
  static b: string
  static c: string
  static setABC(a: string, b: string, c: string) {
    this.a = a
    this.b = b
    this.c = c
    return this
  }
}
class A {
  static prop1: Hey
  static setFields() {
    this.prop1 = Hey.setABC('aee', 'bee', 'cee')
    return this
  }
}
A.setFields()
console.log(A.prop1.a)

Running this code gives the following compile time error in TypeScript:

Property 'a' does not exist on type 'Hey'. Did you mean to access the static member 'Hey.a' instead?

While it works, in JavaScript, you can try the following code snippet:

"use strict";
class Hey {
    static setABC(a, b, c) {
        this.a = a;
        this.b = b;
        this.c = c;
        return this;
    }
}
class A {
    static setFields() {
        this.prop1 = Hey.setABC('aee', 'bee', 'cee');
        return this;
    }
}
A.setFields();
console.log(A.prop1.a);

Click Here for the TypeScript playground if you want to try it out.

I tried to do typecasting for:

console.log((A.prop1 as Hey).a);

But, it also did not work :(

Why is this not allowed in TypeScript when it perfectly works in JavaScript?

8
  • 2
    You want static prop1: typeof Hey, not static prop1: Hey, as shown here. The only reason why you didn't get an error inside setFields() is because your Hey has an empty class instance, which is known to do weird things. Empty classes with only static members is a bit of an antipattern in TS, since the class type is just {}. Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing? Commented Dec 21, 2023 at 20:03
  • @jcalz, yes can you write an answer, and also include why it's an anti pattern? Commented Dec 21, 2023 at 20:07
  • @jcalz, and yes it solves the issue, indeed it should be typeof Hey instead of Hey, (not sure why?) Commented Dec 21, 2023 at 20:08
  • 3
    ①/③ It's an antipattern because there's no point to a constructor you never use to construct things; you can use plain objects for this, it's not Java. And also because empty classes behave strangely, which makes it hard to catch issues like you have run into. It's not catastrophic or anything, and it's mostly opinion so I won't go into it much. ② The type typeof Hey is the type of the value named Hey, which is the constructor and all its static members, while the type Hey is the type of instances of class Hey, which does not have the static members (or any members in your case) Commented Dec 21, 2023 at 20:18
  • 1
    @Normal Yes, don't use classes if you have only static members; see also here Commented Dec 21, 2023 at 20:40

1 Answer 1

2

You've given prop1 the wrong type. It should be:

class A {
    static prop1: typeof Hey;
    ⋯
}

The type Hey refers to the instance type of the class Hey, whereas the type typeof Hey refers to the type of the Hey constructor value. Static properties exist on the constructor and not on instances, so prop1 will be of type typeof Hey, not of type Hey.

If this confuses you, you're not alone. Types and values in TypeScript exist in different namespaces, so you can have a value named X and a type named X and they don't necessarily have anything to do with each other, meaning that typeof X is a different type in general from X, so you have to be careful. See this answer to a similar question for a more in-depth discussion.

Anyway, once you make that change, things start working:

A.setFields()
console.log(A.prop1.a); // okay

Note that the problem was somewhat hidden from you because your Hey class has no instance members. An instance of Hey is indistinguishable in the type system from the empty object type {}, which allows nearly anything to be assigned to it. Empty classes behave strangely in TypeScript and are best avoided. If you have a class, it should probably have instance members. If Hey had even one random property, you'd have seen this error:

class Hey {
    ⋯
    oops = 0; // actual property
}

class A {
    static prop1: Hey;
    static setFields() {
        this.prop1 = Hey.setABC('aee', 'bee', 'cee'); // error!
        //           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Property 'oops' is missing in type 'typeof Hey' but required in type 'Hey'.
        return this
    }
}

Note that if you never intend to construct instances of your class you can often get the desired behavior with plain objects. For example:

const Hey = {
    a: "",
    b: "",
    c: "",
    setABC(a: string, b: string, c: string) {
        this.a = a
        this.b = b
        this.c = c
        return this
    }
}
const A = {
    prop1: Hey,
    setFields() {
        this.prop1 = Hey.setABC('aee', 'bee', 'cee')
        return this
    }
}
A.setFields()
console.log(A.prop1.a); // okay

This behaves quite similarly to your code. One major difference is that I've initialized your properties whereas you left them undefined. But leaving properties uninitialized is a potential null/undefined error waiting to happen. Class instances have --strictPropertyInitialization but that doesn't (yet) apply to static members, although it's been requested at microsoft/TypeScript#27899.

You certainly don't have to refactor away from classes, but TS's support for empty classes with only static members isn't great, as you've seen.

Playground link to code

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

Comments

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.