TypeScript, generics and instanceof

Viewed 819

I have the following code:

abstract class BaseToken {}

class OpenParen extends BaseToken {
    public static assert(t: BaseToken): OpenParen {
        if (t instanceof OpenParen) {
            return t;
        } else {
            throw typeAssertionError;
        }
    }
}

class CloseParen extends BaseToken {
    public static assert(t: BaseToken): CloseParen {
        if (t instanceof CloseParen) {
            return t;
        } else {
            throw typeAssertionError;
        }
    }
}

The assert functions can be used to check whether a given instance of BaseToken is in fact a certain specialization of it.

const t: BaseToken = getTokenFromSomewhere();
const oparen = OpenParen.assert(t); // type of oparen is OpenParen, exception if not

As there are many more classes other than OpenParen and CloseParen derived from BaseToken, this is obviously a lot of boilerplate. I'd like to move the implementation of assert into the base class. The following does not work:

abstract class BaseToken {
    public static assert<T>(t: BaseToken): T {
        if (t instanceof T) { // T used as a value here
            return t;
        } else {
            throw typeAssertionError;
        }
    }
}

class OpenParen extends BaseToken {}
class CloseParen extends BaseToken {}

The problem is that on the one hand, this doesn't compile as instanceof requires a value, not a class, and on the other hand, this approach does not bind T to the subclass type, so OpenParen.assert would still be a generic.

I believe that I could achieve this in C++ with the Curiously recurring template pattern (CRTP), but that doesn't work in TypeScript. Is there some other trick to do the same thing?

3 Answers

Edit

There's a solution that doesn't require an instance. It has still not the exact syntax you want but at least achieves the same this time.

(Credits for the Constructor type to https://www.simonholywell.com/post/typescript-constructor-type.html)

type Constructor<T extends {} = {}> = new (...args: any[]) => T;

function isOfConstructor<T>(cns: Constructor<T>, b: BaseToken): b is T{
    return cns.prototype === b.constructor.prototype;
}

function assert<T>(cns: Constructor<T>, b: BaseToken) : T{
    if (isOfConstructor(cns, b)){
        return b;
    }
    else {
        throw new Error("not of that type");
    }
}

// then call it with
assert(OpenParen, getTokenFromSomewhere());

Original "non-static" solution

If you don't need assert to be static you can get away with

abstract class BaseToken{
    private hasSameTypeAs<T extends BaseToken>(t: BaseToken): t is T{
        return t.constructor.prototype === this.constructor.prototype;
    }

    public assert<T extends BaseToken>(t: T): T {
        if (this.hasSameTypeAs(t)) {
            return t;
        } else {
            throw new Error("foo");
        }
    }
}

Inside a static method, this should refer to the current constructor being used, so the implementation should just check t instanceof this.

Getting the typings to work out is trickier. One problem is that there is no polymorphic this for static methods; see microsoft/TypeScript#5863. You'd like to be able to say that assert() returns a value of the instance type of the constructor you're calling it on. For instance methods you could say that it returns the type named this... but there's no analogous this type for static methods.

There are workarounds, however. Here's one possible way to do it:

abstract class BaseToken {
    public static assert<T extends BaseToken>(this: new (...args: any) => T, t: BaseToken): T {
        if (t instanceof this) {
            return t;
        } else {
            throw new Error("YOU DID A BAD THING");
        }
    }
}

Here we are making assert a generic method where the type parameter T is intended to be the instance type of whatever subclass is being used. And we are using a this parameter to tell the compiler that the you will only call assert() on a constructor object that constructs T instances.


Let's test it out:

class OpenParen extends BaseToken {
    someProp = 123;
}

class CloseParen extends BaseToken {
    otherProp = "abc";
}

const openP: BaseToken = new OpenParen();
const closeP: BaseToken = new CloseParen();

console.log(OpenParen.assert(openP).someProp.toFixed(2)); // 123.00
console.log(CloseParen.assert(closeP).otherProp.toUpperCase()) // ABC

Both openP and closeP are known to the compiler only as BaseTokens, but OpenParen.assert(openP) and OpenParen.assert(closedP) returns OpenParen and CloseParen respectively, and you can access the subclass-specific properties I added to demonstrate that this works.

Playground link to code

Im not sure you can do that in TS as most of TS specific code is removed when transpiled to JS for execution

Here [https://github.com/microsoft/TypeScript/issues/34516] you can see proposal of abstracting statics bur for now - you need to do it a bit around.

For now i would add this to your BaseToken class

  public static assert<T>(t: T extends BaseToken<T>): T {
    throw new Error('[abstract BaseToken method .assert] called');
  };

PS: Yours idea of static class executing non-static method will fail at start, even for yours idea there you need at least one empty instance of each of classes to be able to call non-static method from static ones.

Related