Ben Bergstein

Typescript Primer

May 08, 2020

Background

When I started learning Typescript, I found it difficult to find resources for how to type several common use cases. While there is plenty of documentation, some key techniques for typing Javascript were missing or scattered across the web. In this post, I’ll give examples that I wish I’d found all in one place.

This guide is for anyone starting with Typescript who is hitting a wall on how to express typed code, especially coming from untyped languages. It assumes familiarity with modern Javascript syntax.

Use Cases

In this post, I’ll take a look at the following use cases:

Review: Basic Type Annotations

Let’s quickly cover some examples of scalar types in Typescript.

Typed strings, numbers, arrays

These are your everyday types.

const typedString: string = "bar" // Typed string
const typedNumber: number = 123 // Typed number
const typedArrayOfStrings: string[] = ['foo', 'bar', 'baz'] // Array of strings

Any, void, null, undefined

These tend to be used when typing function argument or return values.

const flexibleConstant: any = 'anything'
const uselessConstant: void = undefined
const undefinedConstant: undefined = undefined
const nullConstant: null = null

Union types

Union types declare a type that can be any of a number of types. This is often useful for arrays with mixed types or functions that take various input.

type stringOrNumberType = string | number
const typedArrayOfStringAndNumbers: stringOrNumberType[] = ['foo', 123, 'baz'] // Array of strings and numbers

How to Type Functions

When I first came to Typescript, typing functions was confusing and frustrating. Let’s start simple!

Number to string

This function takes a number as input and returns the string version of that number:

type convertNumberToStringType = (num: number) => string;
const convertNumberToString: convertNumberToStringType = (num) => '' + num
> convertNumberToString(8)
'8'

Above, the function type is declared before the function itself. While useful when used more than once, in practice, function type signatures are typically declared inline:

const convertNumberToString: (num: number) => string = (num) => '' + num

How to Type Objects: Interfaces

It won’t be long after you start writing Typescript, before you need to declare an object. At this point, I occasionaly found myself scratching my head as to how best to type them.

For me, the answer was interfaces. Interfaces are a way of declaring the structure of an object for typing purposes.

A Basic Interface: Book

Let’s take the example of a book. In Javascript, it might look like this:

{
  title: '1984',
  author: 'George Orwell',
  pages: 328,
}

The Typescript interface for this object would look like this:

“My First Typescript Interface”

interface Book {
  title: string
  author: string
  pages: number
}

To apply this interface as a type annotation to our Javascript object:

const book: Book = {
  title: '1984',
  author: 'George Orwell',
  pages: 328,
}

Nested interfaces: Author

What if the author property requires additional data to the author’s name? Let’s add more properties about the author.

Typescript Interface with Nested Object

interface Author {
  firstName: string
  lastName: string
  born: string
  died: string
  books: Book[]
}

const georgeOrwell: Author = {
  firstName: 'George',
  lastName: 'Orwell',
  born: '1903-06-25',
  died: '1950-01-21',
  books: [],
}

Now let’s set our novel’s author to georgeOrwell:

> const book: Book = {
>  title: '1984',
>  author: georgeOrwell,
>  pages: 328,
>}
Type 'Author' is not assignable to type 'string'.

25   author: georgeOrwell,
     ~~~~~~

Oops! Our Book expects the author to property to be a string. We need to update the Book type to allow an Author type assigned to its author property:

interface Book {
  title: string
  author: Author
  pages: number
}

Interfaces with dynamic key names.

What happens when you have an object but don’t necessarily know the keys? An example of this is indexing data for quicker access.

Here is how to declare an interface without knowing the exact keys, but only the keys’ type:

Object with Unknown keys

interface BookRepository {
  [authorName: string]: Book[]
}

const repo: BookRepository = {
  "George Orwell": [{ title: "1984", pages: 328, author: georgeOrwell }],
  "Herman Melville": [{ title: "Moby-Dick", pages: 704, author: hermanMelville }],
}

The BookRepository type can accept a variety of author’s names as keys, and we are not required to delineate every possible key.

Advanced Function Typing

Let’s take the previous example a step further by adding a function that appends a book to an author’s bibliography.

Function: Publish Book

interface BookInput {
  title: string
  pages: number
}

const publishBook: (author: Author, bookInput: BookInput) => Author = (author, bookInput) => {
  const { books } = author
  const book = { ...bookInput, author }

  return {
    ...author,
    books: [...books, book],
  }
}

Let’s try it out!

> publishBook(georgeOrwell, { title: '1984', pages: 328 })
{
  firstName: 'George',
  lastName: 'Orwell',
  born: '1903-06-25',
  died: '1950-01-21',
  books: [ { title: '1984', pages: 328, author: [Object] } ]
}

💪 Success! The function returns a new representation of the “George Orwell” author, with 1984 appended to the Author’s list of books.

Bonus: Enums and Tuples

Let’s take a quick look at two other, useful types: Enums and Tuples.

Enums: Categorization and State

Often, we have a property or variable with a specific set of possible values. For the above Book scenario, a natural example is genres. For book genres, a (non-comprehensive) enum might look like this:

Enums for categorization

enum Genres {
  Literature = 'Literature',
  Fantasy = 'Fantasy',
  ScienceFiction = 'Science Fiction',
  History = 'History',
  Biography = 'Biography',
}

interface Book {
  ...
  genre: Genres
}

const mobyDick = {
  title: 'Moby-Dick',
  pages: 704,
  author: hermanMelville,
  genre: Genres.Literature,
}

Let’s inspect the genre property:

> mobyDick.genre
"Literature"

That’s right, Moby-Dick is a work of literature 📖.

State

Enums are also useful for representing state:

enum PublicationState {
  Draft = 0,
  Editing,
  Published,
  OutOfPrint,
}

In this example, Typescript automatically sets the enum value of Editing to 1, Published to 2, and so on.

Tuples: Useful for Return Types

Tuples are an array with a finite, ordered array with defined types. While used less frequently than Enums, they are useful for function return types. A specific example is when a function returns multiple values, and we want to allow the consumer of those values flexibility to name them.

Function with Multiple Return Values

type AuthorInfo = [string, number]
const getAuthorInfo: (author: Author) => AuthorInfo = ({ firstName, lastName, books }) => [`${lastName}, ${firstName}`, books.length]

const [melvilleFullName, melvilleBooksCount]: AuthorInfo = getAuthorInfo(hermanMelville)
const [orwellFullName, orwellBooksCount]: AuthorInfo = getAuthorInfo(georgeOrwell)

© 2020 - 2021 Benjamin Bergstein