Typescript static methods in interfaces - javascript

I'm looking for a way to require a class to own some static methods without having to implement them by myself (like an interface can define normal methods). Since interfaces do not support static methods, the following code does not work.
interface MyInterface {
static fromJSON(json: string): MyInterface
toJSON(): object
}
Abstract classes are not the thing I want, because they do not require the developer to write the method himself, but I have to implement it.
Is there something similar to this, without having to write lots of custom logic?
Using the interface from above, the following implementation should not be accepted:
class MyClass implements MyInterface {
// Missing static method "fromJSON"
toJSON() {
return {}
}
}
Neither should this one:
class MyClass implements MyInterface {
static fromJSON(json: string) {
return 123 // Wrong type
}
toJSON() {
return {}
}
}
But this one should be accepted:
class MyClass implements MyInterface {
static fromJSON(json: string) {
return new MyClass()
}
toJSON() {
return {}
}
}

There really isn't much support in TypeScript for constraining the static side of a class. It's a missing feature; see microsoft/TypeScript#14600 for an overall feature request, as well as microsoft/TypeScript#33892 for just the "support static implements on classes" part, and microsoft/TypeScript#34516 for just the "support abstract static class members" part.
One big blocker for something like static members of an interface of the form you've shown is that it's hard for the type system to make sense of it in a way that will actually do what you want. There's a longstanding open issue, microsoft/TypeScript#3841, asking that the constructor property of a class should be strongly typed. Currently it only has the type Function:
class Foo {
instanceProp: string = "i"
static staticProp: string = "s"
}
const foo = new Foo();
foo.constructor.staticProp; // error!
// -----------> ~~~~~~~~~~
// Property 'staticProp' does not exist on type 'Function'
There are some sticky reasons for why this cannot be easily done, spelled out in the issue, but essentially the problem is that subclass constructors are not required to be true subtypes of parent class constructors:
class Bar extends Foo {
subInstanceProp: string;
constructor(subInstanceProp: string) {
super();
this.subInstanceProp = subInstanceProp;
}
}
const bar = new Bar("hello");
Here, the Bar constructor is of type new (subInstanceProp: string) => Bar, which is not assignable to the type of the Foo constructor, which is new () => Foo. By extends, bar should be assignable to Foo. But if bar.constructor is not assignable to Foo['constructor'], everything breaks.
There might be ways around that, but nothing has been implemented so far.
All this means that there's no way to look at an object of type MyInterface and be sure that the thing that constructed it has a fromJSON method. So having static inside interface definitions doesn't really behave in any useful way.
The requests in microsoft/TypeScript#33892 and microsoft/TypeScript#34516, don't have this problem. If you could write this:
class MyClass implements MyInterface static implements MyInterfaceConstructor {
// not valid TS, sorry ------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
toJSON() { return "" };
static fromJSON(json: string) { return new MyClass() };
}
or this:
abstract class MyAbstractClass {
abstract toJSON(): string;
abstract static fromJSON(json: string): MyAbstractClass
// ------> ~~~~~~
// not valid TS, sorry
}
you'd have a way to do this. Alas, neither of those features have been implemented as of TS4.1, so the only way to proceed is with workarounds.
Let's take the MyInterface and MyInterfaceConstructor interfaces I wrote above and see what we can do with them. Right now we can only constrain the instance side via implements MyInterface:
class MyClass implements MyInterface {
toJSON() { return "" };
static fromJSON(json: string) { return new MyClass() };
}
We can't write static implements MyInterfaceConstructor. But we can make a no-op helper function called staticImplements and call it:
function staticImplements<T>(ctor: T) { }
staticImplements<MyInterfaceConstructor>(MyClass); // okay
The fact that this compiles with no error is your guarantee that MyClass's static side is acceptable. At runtime this is a no-op, but at compile time this is valuable information. Let's see what happens when we do it wrong:
class MyClassBad implements MyInterface {
toJSON() {
return ""
}
}
staticImplements<MyInterfaceConstructor>(MyClassBad); // error!
// ------------------------------------> ~~~~~~~~~~
// Property 'fromJSON' is missing in type 'typeof MyClassBad'
// but required in type 'MyInterfaceConstructor'.
class MyClassAlsoBad implements MyInterface {
static fromJSON(json: string) {
return 123 // Wrong type
}
toJSON() {
return ""
}
}
staticImplements<MyInterfaceConstructor>(MyClassAlsoBad); // error!
// ------------------------------------> ~~~~~~~~~~~~~~
// The types returned by 'fromJSON(...)' are incompatible between these types.
function validMyClass(ctor: MyInterfaceConstructor) { }
Those are the errors you were looking for. Yes, the static constraint and the errors are not located exactly where you want them in the code, but at least you can express this. It's a workaround.
There are other versions of this workaround, possibly using decorators (which are sort of deprecated or on hold until decorator support in JS is finalized), but this is the basic idea: try to assign the constructor type to the "static part" of your interface and see if anything fails.
Playground link to code

Related

Problem in creating an abstract class with cause in Typescript

Initially, I want to create an abstract error class with cause field following implementation.
abstract class CustomError extends Error {
constructor(message: string, options: { cause: any }) {
super(message, options);
Object.setPrototypeOf(this, new.target.prototype);
}
}
class NewError extends CustomError {
constructor(message: string) {
super(message, { cause: "helloworld "});
}
}
const a = new NewError("helloworld");
console.log("a", a, "a.cause", a.cause)
However, I got the error
(parameter) options: {
cause: any;
}
Expected 0-1 arguments, but got 2.(2554)
For now this is my workaround, but I would prefer my first implementation to work, to directly use cause field from Error object.
Can anyone explain to me why first version does not work? What is the possible solution to fix it? Thank you.
abstract class CustomError extends Error {
cause: string | undefined = undefined;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
class NewError extends CustomError {
constructor(message: string) {
super(message);
this.cause = "helloworld";
}
}
const a = new NewError("helloworld");
console.log("a", a, "a.cause", a.cause)
If I removed abstract class, it also worked. However, my goal is to able to keep abstract class and make it work.
The cause property of an Error instance was introduced to JavaScript with ES2022. If you want to use this in your TypeScript code, you should increase your --target compiler setting to at least "ES2022" (or at least change your --lib compiler setting to include it). If you do that things will start working.
Keep in mind that if you're trying to support older runtimes then you might want to either polyfill Error (such as this polyfill in core-js) or use your workaround.
Playground link to code with --target of "ES2022".

In Typescript, is it possible to override a superclass property's type without overriding the value?

I have a superclass property with a generic type, e.g.:
class Super {
static get foo(): Record<string, any> {
return ...
}
}
I thought I could override the type like this:
class Sub extends Super {
static foo: SubType;
}
However, this sets Sub.foo to undefined. I want Sub.foo to maintain the same value, but have a new type. Is this possible?
The type for Sub.foo is different because Super.foo depends on other static properties of the subclass.
I think implementing an interface should work, but it'll be good to know if there's another way.
Use declare to avoid compiling the variable into the actual Javascript:
class Sub extends Super {
declare static foo: SubType;
}

Adding abstract/inheritable static constructor helper method on TypeScript class

I have an abstract API class with some base api logic for our app that I am extending in various other classes:
interface Options {
authToken?: string;
ip?: string;
endpointPrefix?: string;
}
abstract class AbstractApi {
private authToken?: string;
private ip?: string;
private endpointPrefix?: string;
constructor({
ip = undefined,
authToken = undefined,
endpointPrefix = '',
}: Options) {
this.authToken = authToken;
this.ip = ip;
this.endpointPrefix = endpointPrefix;
}
protected async get() {
// ...
}
protected async post() {
// ...
}
}
class TodosApi extends BaseApi {
constructor() {
super({ endpointPrefix: '/todos' });
}
getTodos(/* ... */) {
this.post(/* ... */);
}
}
I would like to ensure that every instance of AbstractApi has a static convenience constructor: .new(). e.g. const todos = await TodosApi.new().getTodos().
How would I ensure that each child has this method automatically?
Some things that I have tried but that haven't worked:
static create(data) {
return new this.constructor(data);
}
static create(data) {
return new this(data)
}
It's hard to know exactly what you mean by "haven't worked". Do you mean you got a compiler error? Or that it didn't work at runtime? Or both? Anyway, I'll try to proceed and hopefully address the problem you're facing:
Since the this context of static methods will be a class constructor and not a class instance, you're going to need to use new this() and not new this.constructor(). Assuming that concrete subclasses will have different constructor argument list requirements, your static create() method should probably take a rest parameter. So at runtime you want it to look like this:
static create(...args) {
return new this(...args);
}
Now we just need to come up with appropriate typings for that method to satisfy the compiler and produce reasonable behavior when you try to use it. For instance methods, the obvious solution involves what's called polymorphic this, where the this type represents "whatever the current class instance type is", even for subclasses. Unfortunately, there is (as of TS3.7) no analogous polymorphic this for static methods. Instead, we need to use a workaround involving generics and a this parameter:
static create<A extends any[], R>(this: new (...args: A) => R, ...args: A) {
return new this(...args);
}
That means create will be called as a method on some concrete constructor type which takes some argument list A and produces some instance type R. (You could constrain R to extend AbstractApi but it's not needed here). Let's see if it works:
TodosApi.create().getTodos(); // okay
TodosApi.create("bad argument"); // error: expected 0 arguments, got 1
AbstractApi.create({}); // error: this context of create does not match
So your desired use works fine. You can see that TodosApi.create() requires the same argument list as the TodosApi constructor (namely, no arguments) so you get an error if you pass it the wrong thing. And the compiler gives you an error if you try to call create() directly on AbstractApi, since create() requires its this context to be a constructable thing and AbstractApi is abstract and therefore not constructable.
Does that work for you? Hope that helps; good luck!
Link to code

Implementing an interface in Javascript using flow-runtime

I am using flow-runtime plugin for babel to generate dynamically typechecked javascript code. The following is the workflow I am using
write static javascript code (with flow annotations)
compile this code using babel to convert flow annotations to typechecked code
run this compiled code in node.js
The following workflow gives me an ability to write typescript type code, but with type checking only where I want.
So, now that we understand what I am doing, let me explain what I am trying to achieve
I basically need to build a class called Interface, which will do exactly what it sounds like. This class will be extended by classes that are supposed to be interfaces, and then extended by other classes. Something like this :
class Interface() {
constructor() {
...
}
// interface superclass, supposed to be extended by all interfaces
// this class will provide all the utility methods required
// by an interface, such as validating the implementation of the
// interface, ...
validateInterfaceImplementation() {
...
}
}
// interface definition
class FooInterface extends Interface {
constructor() {
super();
...
}
}
// actual class, that will implement the "FooInterface" interface
class Foo extends FooInterface {
constructor() {
super();
...
}
}
Now, I want to enforce strict implementation of the FooInterface. That means that I want a way to define all the methods that the FooInterface interface expects to be implemented, and validation that all these methods have been implemented by the Foo class.
What I have tried looks something like this
// interface.js
// #flow-runtime
class Interface<T> {
constructor(t: T) {
(this: T); // let flow validate that the interface is implemented
}
}
// FooInterface.js
// #flow-runtime
type foInterface = {
bar(param: string): number;
}
class FooInterface extends Interface<fooInterface> {
constructor() {
super(fooInterface);
}
}
// Foo.js
// #flow-runtime
class Foo extends FooInterface {
}
new Foo(); // should throw an error, because the interface is not implemented
// (the function bar is not defined)
I am facing multiple problems with this approach
I am not sure how to implement the generic class Interface<T>. In think my implementation is incorrect, and the compiled babel code also throws an error, but I can't figure out how to do this.
I am not even sure whether this method will work or not, or whether this is the best way to approach this problem.
Any help would be welcome. Thanks in advance :)
As of flow-runtime 0.5.0 you can use Flow's implements keyword combined with Flow interfaces. I think this will give you what you want without having to create the concrete classes at all:
// #flow
// #flow-runtime
interface IPoint<T> {
x: T;
y: T;
}
interface IJSON {
toJSON (): any;
}
class Point implements IPoint<number>, IJSON {
x: number = 123;
y: number = 456;
toJSON () {
return {x: this.x, y: this.y};
}
}

Typescript definition for ES6 mixins

Is there a way to write a Typescript definition for an ES6 mix-in?
I've this pattern in library.js, and I'd like to create the library.d.ts
// declaration in `library.js`
class Super extends Simple {
constructor() {}
static Compose(Base = Super) {
return class extends Base {
// ...
}
}
}
// usage in `client.js`
class MyClass extends Super.Compose() {}
let myInstance = new MyClass();
class MyOtherClass extends Super.Compose(AnotherClass) {}
No, Typescript type system is not expressive enough for that - see the discussion in https://github.com/Microsoft/TypeScript/issues/7225 and https://github.com/Microsoft/TypeScript/issues/4890.
The idiomatic 'type of classes' in typescript is written as
interface Constructor<T> {
new (...args): T;
}
So one way to write declaration for Compose is
export declare class Simple {}
export declare class Super extends Simple {
static Compose<T>(Base?: Constructor<T>): Constructor<T & {/*mixed-in declarations*/}>
}
That is, Compose return type is declared to be a constructor for intersection type - a type which must have all the properties of parameter (Base) together with all the properties of the mixin.
You can use that declaration (assuming it's in the library.d.ts file) like this
import {Super} from './library'
let MyComposed = Super.Compose(Super)
let myInstance = new MyComposed
The minor inconvenience is that you always have to provide argument for Super.Compose() because type inference does not work without knowing the value for default parameter, and you can't provide value for default parameter in the declaration file.
But the big problem is that you can't really use the result of Compose as a class:
class MyClass extends Super.Compose(Super) {}
does not compile due to the issues mentioned above:
error TS2509: Base constructor return type 'Super & {}' is not a class or interface type.

Categories

Resources