Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Typescript to force generic constraint for interface?

I have 2 interface declarations :

interface IStore    { }
interface SomethingElse     { a: number;}

And 2 classes which implements each:

class AppStoreImplemetion implements IStore 
 { }

class SomethingImplementation  implements SomethingElse
 {
    a: 4;
 }

I want my method to be given the return type as a constraint of "must be IStore" , so I did this:

class Foo {

    selectSync<T extends IStore>( ):  T
        {
        return <T>{/* omitted*/ };    // I set the return type(`T`) when invoking
        }
}

OK

Testing :

This works as expected :

new Foo().selectSync<AppStoreImplemetion>();

But this also works - not as expected :

new Foo().selectSync<SomethingImplementation>();

Question:

How can I force my method to accept a return type which must implement IStore ?

Online demo

like image 526
Royi Namir Avatar asked Mar 29 '18 06:03

Royi Namir


1 Answers

The problem is Typescript usses structural typing to determine type compatibility, so the interface IStore which is empty, is compatible with any other type, including SomethingElse

The only way to simulate nominal typing (the kind you have in C#/Java etc.) is to add a field that makes the interface incompatible with other interfaces. You don't actually have to use the field, you just have to declare it to ensure incompatibility:

interface IStore { 
    __isStore: true // Field to ensure incompatibility
}
interface SomethingElse { a: number; }

class AppStoreImplemetion implements IStore { 
    __isStore!: true // not used, not assigned just there to implement IStore
}

class SomethingImplementation implements SomethingElse {
    a = 4;
}

class Foo {

    selectSync<T extends IStore>(): T {
        return <T>{/* omitted*/ };   
    }
}

new Foo().selectSync<AppStoreImplemetion>();
new Foo().selectSync<SomethingImplementation>(); // This will be an error

Note that any class that has __isStore will be compatible regardless of weather it explicitly implements IStore, again due to the fact that Typescript uses structure to determine compatibility, so this is valid:

class SomethingImplementation implements SomethingElse {
    a = 4;
    __isStore!: true 
}
new Foo().selectSync<SomethingImplementation>(); // now ok

In practice IStore will probably have more methods, so such accidental compatibility should be rare enough.

Just as a side note, private fields ensure 100% incompatibility for unrelated classes, so if it is possible to make IStore an abstract class with a private field. This can ensure no other class is accidentally compatible:

abstract class IStore { 
    private __isStore!: true // Field to ensure incompatibility
}
interface SomethingElse { a: number; }

class AppStoreImplemetion extends IStore { 

}
class Foo {

    selectSync<T extends IStore>(): T {
        return <T>{/* omitted*/ };   
    }
}

new Foo().selectSync<AppStoreImplemetion>(); // ok 

class SomethingImplementation implements SomethingElse {
    private __isStore!: true;
    a = 10;
}
new Foo().selectSync<SomethingImplementation>(); // an error even though we have the same private since it does not extend IStore
like image 63
Titian Cernicova-Dragomir Avatar answered Sep 29 '22 00:09

Titian Cernicova-Dragomir