Typescript utilizes structural typing to determine type compatibility, allowing the empty interface IStore
to be compatible with any other type, such as SomethingElse
.
To mimic nominal typing found in languages like C# and Java, you can introduce a field that makes the interface incompatible with others. This field doesn't need to be used, just declared for ensuring incompatibility:
interface IStore {
__isStore: true // Field for ensuring incompatibility
}
interface SomethingElse { a: number; }
class AppStoreImplementation implements IStore {
__isStore!: true // Simply there to implement IStore
}
class SomethingImplementation implements SomethingElse {
a = 4;
}
class Foo {
selectSync<T extends IStore>(): T {
return <T>{/* omitted*/ };
}
}
new Foo().selectSync<AppStoreImplementation>();
new Foo().selectSync<SomethingImplementation>(); // Will result in an error
It's important to note that any class with __isStore
will be considered compatible regardless of explicit implementation of
IStore</code, due to Typescript's structural type system.</p>
<pre><code>class SomethingImplementation implements SomethingElse {
a = 4;
__isStore!: true
}
new Foo().selectSync<SomethingImplementation>(); // now valid
In actual use, IStore
likely includes more methods, reducing accidental compatibility instances.
Private fields ensure complete incompatibility between unrelated classes. Consider defining IStore
as an abstract class with a private field to prevent accidental compatibility:
abstract class IStore {
private __isStore!: true // Field for ensuring incompatibility
}
interface SomethingElse { a: number; }
class AppStoreImplementation extends IStore {
}
class Foo {
selectSync<T extends IStore>(): T {
return <T>{/* omitted*/ };
}
}
new Foo().selectSync<AppStoreImplementation>(); // Valid
class SomethingImplementation implements SomethingElse {
private __isStore!: true;
a = 10;
}
new Foo().selectSync<SomethingImplementation>(); // Error since it does not extend IStore despite having the same private field