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);