I have an application with various kinds of IDs that I pass around as ID objects to enforce type safety. I now want to inherit from the ID base class to differentiate between various kinds of IDs and would like TypeScript to check whether I use the right ID with the right functions and variables. Consider the example below:
class Id {
id: string
constructor(id: string) {
// enforce invariants here or throw
this.id = id
}
}
class VideoId extends Id {
constructor(id: string) {
super(id)
}
}
class UserId extends Id {
constructor(id: string) {
super(id)
}
}
function checkUser(userId: UserId) {
console.log("It's a user!")
}
const videoId = new VideoId("dQw4w9WgXcQ")
// Should be illegal but works!
checkUser(videoId)
The point of the subclasses is to leverage the type checking and prevent calls to checkUser
with anything other than a UserId
as an argument. However, since TypeScript will allow any structurally identical object as a class instance, the above example compiles.
How do I differentiate the subclasses from the main class with the least boilerplate possible? Ideally, the added code would not appear in the compiled JavaScript and just impact type checking, so I may be looking for something similar to Rust's PhantomData
.
CodePudding user response:
Thanks to @jcalz in the comments for the declare
idea, I came up with this fairly clean solution with no runtime impact:
class Id<Type extends string> {
id: string
declare readonly __type: Type
constructor(id: string) {
// enforce invariants here or throw
this.id = id
}
}
class VideoId extends Id<'video'> {}
class UserId extends Id<'user'> {}
function checkUser(userId: UserId) {
console.log("It's a user!")
}
const videoId = new VideoId("dQw4w9WgXcQ")
// Error! Type '"video"' is not assignable to type '"user"'.
checkUser(videoId)
CodePudding user response:
You need to have some distinguishable attribute type between the subclass of Id
. The easiest solution I'd go for would be to define a IdType
enum, use it on Id
parent class as type: IdType
field and narrow typing on sub-classes like this:
enum IdType {
Video,
User,
}
class Id {
id: string;
type: IdType;
constructor(id: string) {
// enforce invariants here or throw
this.id = id;
}
}
class VideoId extends Id {
type: IdType.Video = IdType.Video;
constructor(id: string) {
super(id);
}
}
class UserId extends Id {
type: IdType.User = IdType.User;
constructor(id: string) {
super(id);
}
}
function checkUser(userId: UserId) {
console.log("It's a user!");
}
const videoId = new VideoId('dQw4w9WgXcQ');
const userId = new UserId('dQw4w9WgXcQ');
// valid
checkUser(userId);
// invalid
checkUser(videoId);