Elegantne rozlišujte podtriedy TypeScriptu
Mám aplikáciu s rôznymi druhmi ID, ktoré posielam ako ID objekty, aby som zabezpečil typovú bezpečnosť. Teraz chcem dediť od základnej triedy ID, aby som rozlíšil rôzne druhy ID, a chcel by som, aby TypeScript kontroloval, či používam správne ID s príslušnými funkciami a premennými. Pozrite si príklad nižšie:
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)
Cieľom podtried je využiť kontrolu typov a zabrániť volaniam checkUser s čímkoľvek iným než UserId ako argumentom. Avšak pretože TypeScript umožní použiť akýkoľvek štrukturálne identický objekt ako inštanciu triedy, vyššie uvedený príklad sa skompiluje.
Ako odlíšim podtriedy od hlavnej triedy s čo najmenším množstvom opakovane písaného kódu? Ideálne by bolo, keby pridaný kód nebol viditeľný v skompilovanom JavaScripte a ovplyvňoval iba kontrolu typov, takže možno hľadám niečo podobné ako PhantomData v Ruste.
Odpoveď č. 1
Vďaka @jcalz v komentároch za nápad s declare, prišiel som s celkom čistým riešením bez vplyvu na beh programu:
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)
Odpoveď č. 2
Musíte mať nejaký rozlišovací typ atribútu medzi podtriedou Id. Najjednoduchšie riešenie, ktoré by som zvolil, by bolo definovať výčtový typ IdType, použiť ho v rodičovskej triede Id ako pole type: IdType a zúžiť typovanie v podtriedach nasledovne:
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);
