Edument

TypeScript generics in a dark castle

In this post we explore TypeScript generics, using an example from the Angular course where the participants get to build their own Dark Castle...

Welcome to the castle!

So - JavaScript! Fans of the language praise the flexibility. Opponents argue that the headaches of loose types, which is part of what makes that flexibility possible, are too severe to be worth it.

Then - TypeScript! Typescript adds strong types to JavaScript, without sacrificing almost any of the flexibility. At Edument we’ve been collectively wooed by how easily TypeScript elevates our JavaScript coding experience to the next level.

A very powerful feature of strong type checking is generics. It is also a feature that is commonly hard to grasp, so in this post we’ll now explore an illustrative example!

In our Angular course we often coax the participants into making an app version of a classic “choose-your-own-adventure” gamebook:


They get to define their adventure as JSON data, looking something like this:

const theDarkCastle = {  
   beginning: { 
   title: "The journey begins",    
      description: "You wake up in a strange room. …",    
      options: [     
          { text: "Open the door", targetPage: 'hallway' },      
          { text: "Climb out the window", targetPage: 'precipice' },   
      ]  
   },  
   hallway: {    
      title: "A dark hallway",    
      description: "The hallway ends with two doors.",    
      options: [      
          { text: "The door on the left", targetPage: 'guardroom' },      
          { text: "The door on the right", targetPage: 'staircase' },      
          { text: "Go back", targetPage: 'beginning' },    
      ]  
   },  
   // ...massa fler sidor...
}


Being able to whip up large objects like that, without going via a predefined class, is a good example of JavaScript’s flexibility.

But! Without tooling it would be very easy to make mistakes while typing out that data. Therefore we make the participants define a TypeScript interface for their adventure, looking something like this:

type Adventure = {  
   [pageId: string]: {    
      title: string,    
      description: string,   
      options: {      
          text: string      
          targetPage: string    
      } [ ] 
    }
}


Now, by type-casting the data as an Adventure, we’ll be warned if we make a mistake as we type:



Note the angry red squiggly line because we misspelt “description”.

However, there’s one place in our adventure where we won’t get help - the targetPage parts in the options that points to other pages! TypeScript thinks they’re just strings now, but in reality they’re strings that also has to be existing keys in our adventure object!

Now, we could make a special type for our page id:s…

type DarkCastlePageId = 'beginning' | 'hallway' | 'hallway2' | 'precipice' // ...och massa fler


...and use that in our adventure type instead:

type Adventure = {  
   [pageId in DarkCastlePageId]: {    
      title: string,    
      description: string,    
      options: {      
          text: string      
          targetPage: DarkCastlePageId    
      } [ ]  
   }
}


Now TypeScript can save us if we type “precipise” instead of the correct key “precipice”:



Victory! But, we have a problem. The app we’re building is meant to be like a game engine - it should be able to run ANY adventure, as long as the data has the correct shape. But now we’ve hard-coded the page id:s for The Dark Castle into the definition of what an adventure should look like!

This is where generics come in. They let us pass arguments to types!

type Adventure<PageIdType extends string = string> = {  
   [pageId in PageIdType]: {    
      title: string,    
      description: string,    
      options: {      
          text: string      
          targetPage: PageIdType    
      } [ ]  
   }
}


Essentially we just said that…

  • The Adventure type takes a parameter type
  • That parameter needs to be an extension of the string type
  • If nothing is passed in, default to string

We now type the Dark Castle adventure by passing in our previously created DarkCastlePageId type to the Adventure type...

const theDarkCastle: Adventure<DarkCastlePageId> = {  
   beginning: {    
      title: "The journey begins",


...and you can of course type your own adventure by passing in a different argument, yet still run it in the same engine!

const myLittlePony: Adventure<MyLittlePonyPageId> = {  
   stable: {    
   title: "In the stable",

In the Angular course we’ll rarely go beyond what you’ve just seen (although once or twice we’ve shown the dark magic with which you can autogenerate the pageId types), since TypeScript is just a tool for the main dish which is Angular.

But in the TypeScript course we have no such limits! There we take our participants deep down the rabbit hole, exploring generics and other typing tools in depth.


Författare: David Waller

JavaScript seem to be disabled in your browser.

You must have JavaScript enabled in your browser to utilize the functionality of this website.