Typescript abstract class static method not enforced
Asked Answered
I

2

16

I have this simple code in TypeScript:

abstract class Config {
    readonly NAME: string;
    readonly TITLE: string;

    static CoreInterface: () => any
}

class Test implements Config {
    readonly NAME: string;
    readonly TITLE: string;
}

Even though the CoreInterface() member is missing in the Test class, TypeScript does not complain. Why is this?

I need every derived class to provide some metadata about itself in the CoreInterface() static function. I know I could just extend the Config class and have each sub-class provide its own implementation of CoreInterface(), but I do not want sub-classes to automatically inherit any of the members of the COnfig class. That is why I use "implements" instead of "extends"

Interlace answered 1/5, 2017 at 17:17 Comment(8)
Because it's not a member. It's a static field, which thus belongs to the class, and not to the instance of the class.Petrozavodsk
So is there no way to enforce that any class derived from Config MUST implement CoreInterface()?Interlace
abstract class Config implements CoreInterface? Is that what you want?Petrozavodsk
And how will you represent CoreInterface()? Can you post example code of your solution here?Interlace
Implementing an interface means: any instance of a class that implements Config will have these instance fields and instance methods. Since CoreInterface is a static member, it's irrelevant. Static members belong to classes, not instances. And they're not called polymorphically. If you're trying to impose that every implementation must have a given static member, then AFAIK, it's not possible (and I don't understand what purpose it would serve). What are you trying to achieve?Petrozavodsk
No, I simply want to indicate that any class that derives from Config must implement the specific static function CoreInterface()Interlace
>> "it's not possible (and I don't understand what purpose it would serve). What are you trying to achieve?" I need every derived class to provide some metadata about itself in the CoreInterface() static function. I realizes I could probably just extend the super class and have each sub class provide its own implementation of CoreInterface(), but I do not want want sub-classes to automatically inherit any of the members of the super-class. That is why I use "implements" instead of "extends"Interlace
AFAIK, that's not possible. But I would probably use the reverse strategy: have a collection of metadata objects, all having a class extending Config (or some factory function allowing to create a Config subclass instance)Petrozavodsk
P
31

Based on your comment, here's how you can achieve what you're looking for:

interface ConfigConstructor {
    CoreInterface: () => any;
    new (): Config;
}

interface Config {
    readonly NAME: string;
    readonly TITLE: string;
}

const Test: ConfigConstructor = class Test implements Config {
    readonly NAME: string;
    readonly TITLE: string;

    static CoreInterface = function (): any { return "something"; }
}

(code in playground)

If you comment out one of the members (i.e.: NAME) you'll get this error:

Class 'Test' incorrectly implements interface 'Config'.
Property 'NAME' is missing in type 'Test'.

If you comment out the static CoreInterface you'll get this error:

Type 'typeof Test' is not assignable to type 'ConfigConstructor'.
Property 'CoreInterface' is missing in type 'typeof Test'.


Original answer

Static members/methods don't work with inheritence (that's true to OO in general and not specific to typescript) because (as @JBNizet commented) all static properties belong to the class itself and not to the instances.

As written in Wikipedia article:

A static method can be invoked even if no instances of the class exist yet. Static methods are called "static" because they are resolved at compile time based on the class they are called on and not dynamically as in the case with instance methods, which are resolved polymorphically based on the runtime type of the object. Therefore, static methods cannot be overridden

Also check this thread: Why aren't static methods considered good OO practice?

As for what you want to accomplish, you won't be able to get compilation errors for not implementing the static method when extending the class, but you can get runtime errors:

class A {
    static fn() {
        throw new Error("not implemented!");
    }
}

class B extends A {
    static fn() {
        console.log("B.fn");
    }
}

class C extends A { }

B.fn(); // ok
C.fn(); // error: not implemented!

(code in playground)

Pasty answered 1/5, 2017 at 17:45 Comment(11)
Your answer is informative, but remember that I am not trying to do inheritance here (that is why I use implements rather than extends. Also, TypeScript is just trying to provide some type-checking and intellisense for JavaScript, JavaScript does not really have class-based inheritance. Therefore, what I am trying to do does not really have to map to the underlying JS; I just want a way to express that a set of classes must all have certain features. There is no real underlying reason why TypeScript cannot provide this check.Interlace
@Interlace Well, it'll have to map to the underlying JS, since it's always just JS. Just because there's no reason TS couldn't do this doesn't mean it should, since it would break its object model (similar to many other object models).Magdalenmagdalena
Check my revised answerPasty
@Dave: A lot of the type information is discarded by TypeScript when it emits JS; the type info is there mostly for static analysis of the code, not for runtime code generation.Interlace
@Nizan Tomer: Your code works, amazing! I'm curious as to why exactly it works, though. I would have assumed the CoreInterface() function in Test did not have to be static to satisfy the ConfigConstructor interface, but it does. How does that work?Interlace
Also, @Nizan Tomer, you could omit naming the class assigned to the Text constant and the code would still be valid, i.e., const Test: ConfigConstructor = class implements Config { ...Interlace
Yeah, you can omit the 2nd Test and it works just the same, I prefer it with it but that's a personal taste. As for why Test.CoreInterface needs to be static: because ConfigConstructor defines the static parts (or the class part) while the actual class defines (and implements) the instance part. It's the same with Array and ArrayConstructor, and Promise and PromiseConstructor (etc..)Pasty
@Interlace Yes, I know; that's what I mean by "it's always just JS".Magdalenmagdalena
Is there a way to have ConfigConstructor accept a type parameter? I tried interface ConfigConstructor<T implements Config> and new (): T, but when passing Test it refers to the value being defined, rather than the class. If there's a way to do this, TypeScript would know that new Test() is a Test, not just a Config.Jago
@JohnLeuenhagen without diving too much into it, I'd say that one solution is to create an interface for Test and then do const Test: ConfigConstructor<Test> ....Pasty
Thanks, but what if you need to apply the decorator to the class?Cimbri
N
1

I have been struggling with this dilema aswell, and came up with a solution that combines these concepts:

  1. Singleton pattern
  2. Abstract function
  3. Static function
  4. Generic types

End goal was to force all Derivative classes to implement a single function which are being used by a static function to store an static object which are being served statically.

I don't know if this is a hack, but it works.

export type StaticThis<T> = { new (): T, product: Product };

export default abstract class SingletonProduct {

  static product: Product;
  
  // Derivative classes must implement this function
  abstract createProduct(context: Context): Product;

  // This function ensures singleton behavior and ensures only one instance of the product
  static getProduct<T extends SingletonProduct>(this: StaticThis<T>, context: 
  Context): Product {
    if (!this.product) {
        this.product = (new this()).createProduct(context)
    }
    return this.product
  }
}

Derived class:

export default class DerivedProductClass extends SingletonProduct {

  createProduct(context: Context): Product {
    return new Product(...)
  }

}

Usage:

DerivedProductClass.getProduct(ctx)
Novercal answered 27/6, 2022 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.