2

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 3

1

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

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

Comments

1

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");
        }
    }
}

1 Comment

Ehm, unfortunately that's not what I want, because this requires two instance, one to call assert on and the other to pass as argument. But I have only one instance and want to ensure it has the proper type, or fail.
0

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.

2 Comments

I don't understand how this could help my problem. I still need to override this in the subclass as this function will always throw.
This makes sure that if you (in future) forget to add assert you would get error. Thats really it - as we need to wait for support of abstract in statics - and as in tthe topic link i gave you - it might take a while...

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.