In the code snippet, the function doSomething should accept only the objects of type Base and not other types that match the type structure something like an instanceOf constraint.
interface IBase {
prop1: string;
}
abstract class Base implements IBase {
constructor() { }
prop1: string = '';
}
class Derived1 extends Base { }
class Derived2 extends Base { }
class AnotherBase {
constructor() { }
prop1: string = '';
}
// T extends Base
function doSomething<T extends Base>(obj: T) {
// do something
}
//cases should work
doSomething(new Derived1());
doSomething(new Derived2());
// cases should NOT work
doSomething({ prop1: 'test' }); // works
doSomething(new AnotherBase()); // works
How can we make the function strictly accept only objects of type Base? We can understand the present working as per the type-compatibility rules. But, is there an approach to specify a generic constraint to match exact type?
UPDATE:
From the answers to this and similar questions, It looks impossible to achieve this compile-time. I decided to include the instanceOf check to achieve the expected behaviour at runtime.
// T extends Base
function doSomething<T extends Base>(obj: T) {
if(!( obj instanceof Base)){
console.log('not a Base type');
throw new Error('An instance of Base is expected.');
}
// do something
}
//cases should work
console.log('Derived1');
doSomething(new Derived1());
console.log('Derived2');
doSomething(new Derived2());
// cases should NOT work
console.log('{..}');
doSomething({ prop1: 'test' }); // doesn't work
console.log('AnotherBase');
doSomething(new AnotherBase()); // doesn't work
Short answer: Nope.
Typescript is structural, not nominal. That makes that if two types has the same interface, then they are the same type. That even includes class instances versus object literals.
We can test this with something like:
type Exact<A, B> = A extends B ? B extends A ? true : false : false
This type alias takes two parameters. If A extends B, and vice versa, then we know typescript considers them to be identical.
type IBase_Base = Exact<IBase, Base> // true
type Base_Derived = Exact<Base, Derived> // true
type Derived_ObjLiertal = Exact<Base, { prop1: string }> // true
So class Derived extends Base { } is identical to Base as far as typescript is concerned. in fact a class instance and an object literal are considered identical as long as the instance has the shape as the literal.
That said, if the derived class was different in anyway, then you could notice that with the help if this Exact type and forbid it.
First you could make a type to enforce that a type exactly match, like:
type EnforceExact<Constraint, T> = Exact<Constraint, T> extends true ? T : never
Then make your function use this helper:
function doSomething<T extends IBase>(obj: EnforceExact<IBase, T>) {
// do something
}
Now if we make the Derived class different somehow:
class DerivedWithAddition extends Base {
foo: number = 123
}
Then we get a type error when we try to pass this in:
doSomething(new DerivedWithAddition()) // not assignable to parameter of type 'never'.
Playground
You could also use the brand workaround as suggested in the answer that @kaya3 linked. However, unless there's a very good reason, you probably shouldn't. This structural equivalence is a pretty great feature of typescript. Your life will be easier if you just let someone pass in whatever they want, as long as it has the correct interface.
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