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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With