7

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

1
  • 1
    You cant. Typescript has duck typing. However i think typescript would benefit from unduckable types, so if you request it as a feature, i will support you :) Commented Mar 29, 2018 at 6:17

1 Answer 1

4

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
Sign up to request clarification or add additional context in comments.

4 Comments

Sorry but what's that !: ?
@RoyiNamir I use strict when I compile, so I got an error that the fields was not initialized ! tells the compiler not to worry about the uninitialized field.
Oh. TIL. Thanks.
@RoyiNamir also added the option for 100% incompatibility with abstract classes and a private field. Hope it helps, good luck

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.