Dan D Kim

Let's share stories

Typescript Generics for those that have been avoiding it

2020-11-16 Dan D. Kimtypescript

Typescript has been very fun to learn, but there is one concept that I found very daunting:

Generics.

Meme about avoiding generics

I have been avoiding it in the beginning, but decided to tackle it and document my understandings here.

Basics in 5 minutes

Let’s start with a simple Javascript function. We will use this example to play around with generics.

function makeInfo () {
  let info

  function getInfo () {
    return info
  }

  function setInfo (val) {
    info = val
  }

  return { getInfo, setInfo }
}

makeInfo() returns an object with two functions getInfo() and setInfo(). Pretty simple, right?

const { getInfo, setInfo } = makeInfo

setInfo('git')
console.log(getInfo()) // git

setInfo('foo')
console.log(getInfo()) // foo

Let’s start enforcing types with generics.

Restrict type to a string

Let’s restrict the type of info to a string.

function makeInfo () {
  let info: string
  function getInfo () {
    return info
  }

  function setInfo (val: string) {    info = val
  }

  return { getInfo, setInfo }
}

Now the type of info is restricted to string.

const { getInfo, setInfo } = makeInfo

setInfo('foo') // <--- works
setInfo(1) // <--- compilation error

Restrict info type to string or number

How do we restrict the info type to be of string, or of number?

Option 1

The following might come as a natural guess.

function makeInfo () {
  let info: string | number
  function getInfo () {
    return info
  }

  function setInfo (val: string | number) {    info = val
  }

  return { getInfo, setInfo }
}

But it IS NOT PRACTICAL.

Why isn’t it practical? Because it allows both a number or a string, whereas we want two different states of info: one for a string and one for a number.

What the hell am I talking about? Maybe the following code block helps. The stuff below is sort of what we want.

const stringOnlyInfo = makeInfo()
const numberOnlyInfo = makeInfo()

stringOnlyInfo.setInfo('foo')
stringOnlyInfo.setInfo(1) // <--- want this to be disallowed

numberOnlyInfo.setInfo(1)
numberOnlyInfo.setInfo('foo') // <--- want this to be disallowed

How can we do that?

Option 2

Use generics!!

function makeInfo<T> () {  let info: T
  function getInfo () {
    return info
  }

  function setInfo (val: T) {    info = val
  }

  return { getInfo, setInfo }
}

<T> is just a type you pass into a function.

const numInfo = makeInfo<number>() // info can only be a number

numInfo.set('foo') // <--- will throw compilation error

Code block screenshot

const stringInfo = makeInfo<string>() // set info to be a string only

stringInfo.set(1) // <--- will throw compilation error

I chose T for Type, but you can choose anything you want.

Common names:

  • E for Element
  • S for State
  • T for Type
  • K for Key
  • V for Value

Problem: <T> could also be a boolean, or any other type

Nothing is stopping us from using other types.

makeInfo<boolean>() // <--- will still work
makeInfo<Array>() // <--- will still work

But we want to restrict makeInfo() to just string and number.

How do we do that?

Use extends

function makeInfo<T extends number | string>()

Now, makeInfo() will only accept number or string.

makeInfo<number>() // ok
makeInfo<string>() // ok
makeInfo<boolean>() // <--- will throw compilation error

Tip: Default type

You can specify a default type the same way you do for function arguments.

function makeInfo<T extends number | string
  = string>
makeInfo() // <--- of type string
makeInfo<number>() // <--- need to explicitly declare number type for number info

That’s it for the basics! Hopefully this helps you better read Typescript code.

At first, the plethora of all the <T>, <S>, <K, V> might look intimidating, but at the end of the day they are just simple type definitions.

Happy programming!