Reputation: 6715
I have two classes, A and B.
class A {
id: number;
}
class B {
id: number;
}
Is it possible to have a function typed so that it only accepts id from class A? Maybe by changing number
to a custom type of some sort?
Upvotes: 1
Views: 378
Reputation: 327624
It sounds like you want the id
property of class A
to be of a nominal type compatible with number
. If this type is declared as ClassANumber
, it would be considered distinct from a regular number
type, because they have different declarations and different names. So your ideal behavior might look like this:
class A {
id: ClassANumber;
constructor(id: ClassANumber) {
this.id = id;
}
}
class B {
id: number;
constructor(id: number) {
this.id = id;
}
}
function acceptClassAId(id: ClassANumber) { }
acceptClassAId(new A(1).id) // okay
acceptClassAId(new B(1).id) // error, number is not a ClassANumber
Unfortunately, TypeScript's type system is structural, and not nominal. If the types number
and ClassANumber
have the same structure (that is, they both behave like a number
when used), then the compiler will consider them the same type:
type ClassANumber = number;
acceptClassAId(new A(1).id) // no error, as desired
acceptClassAId(new B(1).id) // ALSO no error, 😢
There is no error here because ClassANumber
is the same type as number
, and so new B(1).id
's type, number
, is assignable to ClassANumber
.
There are various tricks you can use to simulate nominal types in TypeScript; you can see the Nominal Typing section of @basarat's TypeScript Deep Dive, or the TypeScript FAQ entry for "Can I make a type alias nominal"?. The canonical place to discuss/consider/lament this is probably microsoft/TypeScript#202, an open issue since 2014.
The one I'll present here is from the "Can I make a type alias nominal" FAQ entry. It uses so-called type branding to intersect the number
primitive with an object type containing a single property which can be used to distinguish number
from ClassANumber
:
type ClassANumber = number & { __classAId: true };
Now the compiler can tell the difference:
acceptClassAId(new A(1).id) // okay
acceptClassAId(new B(1).id) // error!
// Argument of type 'number' is not assignable to parameter of type 'ClassANumber'.
This is just a workaround, of course. Technically you're lying to the compiler: at runtime, a ClassANumber
will just be a number
, without a __classAId
property. And so there are various warts that crop up at compile time. For example, since there's no way to turn a number
into a ClassANumber
for real, you'll have to use something like a type assertion in any code that needs to do this:
class A {
id: ClassANumber;
constructor(id: number) {
this.id = id as ClassANumber; // need to assert here
}
}
And once you do have something the compiler believes is a ClassANumber
, you'll see this phantom __classAId
property you need to steer clear of:
new A(2).id.__classAId.valueOf(); // accepted by compiler, but error at runtime
So proceed with caution.
Upvotes: 1