Generics

Imagine that you are developing a game and storing and retrieving game information to/from a database. This means you have to implement the classic Create, Read, Update and Delete operations (CRUD) for the various objects in the game. Here’s some high level code that beings to implement the game and this logic:

The code defines three classes:

  • : This is the game object itself, keeping track of overall game state, including a list of players and the currently active player.
  • Player: Represents a player in the game. Players also have some state information, although its different than a Game.
  • GameStateDBHelper: A utility class that provides input/output operations and supports all four CRUD operations for the Game object.

GameStateDBHelper defines four public methods, one for each of the CRUD operations. These each take commonsense input parameters and return commonsense results. Consider LoadGame:

  1. public LoadGame(query: string): Game {
  2. // Use supplied query to load some game state from the database.
  3. // Convert that to a GameState object
  4. // return it
  5. return new Game(null);
  6. }

LoadGame is passed a query (think “select * from Games…”). It parses the result and returns back a new Game object. Obviously, there’s a lot of hand waving going on in the example, but hopefully the concept is clear.

Despite the clarity and strong-typed goodness, this approach is nonetheless problematic. We already know we’ll want another database-backed entity - Player. If we simply follow the current approach, we end up creating a new helper function, PlayerStateDBHelper. It has to provide the same CRUD functions and each one shaped almost identically to GameState. “Shape” in this case means:

  • Looking up database connection information.
  • Accessing the database
  • Executing some common command that varies only in small details from one object to another
  • Managing errors
  • Returning success/fail messages

We can mitigate most of that using TypeScript’s generic functionality. Here’s how it would look like:

  1. interface DBBackedEntity {
  2. TableName: string;
  3. }
  4. class GameState implements DBBackedEntity {
  5. public get TableName(): string { return this.myDBTableName; }
  6. public CurrentPlayerIndex: number;
  7. constructor(someGameState: any) {
  8. this.myDBTableName = "Games";
  9. }
  10. }
  11. class GamePlayer implements DBBackedEntity {
  12. private myDBTableName: string;
  13. public get TableName(): string { return this.myDBTableName; }
  14. public PlayerName: string;
  15. public Score: number;
  16. constructor(somePlayerState: any) {
  17. this.myDBTableName = "Players";
  18. }
  19. class DBHelper<T extends DBBackedEntity> {
  20. public CreateRecord() : T { return null; }
  21. public ReadRecord(query: any): T { return null; }
  22. public DeleteRecord(basedOn: T): boolean { return true; }
  23. public UpdateRecord(basedOn: T) : boolean { return true; }
  24. const gameStateHelper = new DBHelper<GameState>();
  25. const gamePlayerHelper = new DBHelper<GamePlayer>();
  26. const newPlayer = gamePlayerHelper.CreateRecord();
  27. console.log(`New player score: ${newPlayer.Score}.`)
  28. const existingGameState = gameStateHelper.ReadRecord("some query goes here");
  29. const newGameState = gameStateHelper.CreateRecord();
  30. const deleteResult = gameStateHelper.DeleteRecord(newGameState);
  31. const updateResult = gameStateHelper.UpdateRecord(existingGameState);

Generics introduce some new syntax and leverage existing concepts (like interfaces) in new ways.

The code first defines a new interface, DBBackedEntity. This interface requires a single text field, “TableName”. This obviously maps to a database table via its name.

The DBHelper class introduces the generics syntax:

This syntax, <T extends DBBackedEntity> effectively says, “The DB helper class works against any type (class) that implements the DBBackedEntity interface.” When client code instantiates an instance of DBHelper, it will specify a value for that parameter, T. These two lines show how to pass a value for T:

  1. const gameStateHelper = new DBHelper<GameState>();
  2. const gamePlayerHelper = new DBHelper<GamePlayer>();

When working with generics, we supply type parameters via angle brackets: DBHelper<GameState> and DBHelper<GamePlayer>. TypeScript replaces the T parameter in the DBHelper class with GameState and respectively.

Further Reading

Summary

This chapter on generics brings the main body of of Yet Another TypeScript Book to a close. The next chapter suggests some additional reading and videos that you may find of interest