TypeScript Fundamentals

Published Jul 7, 2021

Table of Contents

Youā€™re Already Using TypeScript

If youā€™re a web developer, youā€™re probably using Visual Studio Code (not to be confused with Visual Studio) and if youā€™re not at least consider it.

Have you ever asked yourself how code completion in your editor works?

example.js
const pokemon = ['Bulbasaur', 'Charmander', 'Squirtle']

Type on hover

The editor knows that pokemon is an array of strings.

Intellisense

Since pokemon is an array the editor intellisense is smart enough to only show us array methods.

Imagine not having this feature. You would have to remember and look every method up on the MDN Web Docs.

Signature call

If we look at the map array method we can see itā€™s call signature and description in the tooltip.

This is JavaScript ā€” yet itā€™s using TypeScript under the hood.

TypeScript isnā€™t just types ā€” itā€™s popular because this is the kind of developer experience that every other developer is used to in other languages.

We can add @ts-check at the top of a JavaScript file to enable type checking in JavaScript.

example.js
// @ts-check

const pokemon = ['Bulbasaur', 'Charmander', 'Squirtle']

pokemon.push(1) // oops! šŸ’©

TypeScript error

The pokemon array contains only strings, so we get a type error when trying to assign number to string.

The VS Code editor uses the TypeScript language server under the hood.

Built-in extensions

If we look at the built-in VS Code extensions, we can notice some usual suspects. Look, thereā€™s Emmet. šŸ˜„

The language server provides us with sophisticated features such as code completion, refactoring, syntax highlighting, and error and warnings.

The Language Server Protocol allows for decoupling language services from the editor so that the services may be contained within a general-purpose language server. Any editor can inherit sophisticated support for many different languages by making use of existing language servers. Similarly, a programmer involved with the development of a new programming language can make services for that language available to existing editing tools. ā€” Language Server Protocol (Wikipedia).

Visual Studio Code and TypeScript are made by Microsoft, so that explains the tight integration. This doesnā€™t mean youā€™re left in the dark if youā€™re using another editor like WebStorm or Vim. The TypeScript language server is available as a plugin for those editors.

Your editor already has some great features that get enhanced by TypeScript:

  • Auto imports (as you type imports get added)
  • Code navigation (definitions, lookup)
  • Rename (rename symbols across file)
  • Refactoring (extracting code to functions)
  • Quick fixes (suggested edits like fixing a mispelled property name)
  • Code suggestions (for example converting .then to use async and await)

You can learn more about these features in-depth if you read the documentation for the JavaScript language.

Why Should You Use TypeScript?

When writing JavaScript we donā€™t get a lot of information before we run our code.

Thereā€™s no way for us to know if the JavaScript code has errors until we see the result on the page and go back to our code editor to fix the mistake and rerun the code.

example.js
const pikachu = {
  name: 'Pikachu',
  weight: 60
}

pikachu.weigth // oops! šŸ’©

No checking

To prove my point Iā€™m sure you barely noticed the mistake and had to look at what it was. šŸ˜„

The editor didnā€™t warn us about mispelling weight.

Imagine the same scenario with an API where you pass wrong arguments to a method ā€” you just hope it works.

example.ts
const pikachu = {
  name: 'Pikachu',
  weight: 60
}

pikachu.weigth // šŸ¤” Did you mean 'weight'?

Checking

Code completion already makes it hard to make such a mistake writing regular JavaScript because of the benefits we get from TypeScript under the hood, but when we do thereā€™s nothing to warn us.

The most common kinds of errors that programmers write can be described as type errors: a certain kind of value was used where a different kind of value was expected. This could be due to simple typos, a failure to understand the API surface of a library, incorrect assumptions about runtime behavior, or other errors. ā€” TypeScript Handbook

TypeScript only gives us information before we run the code. Thatā€™s also known as static type checking.

Static type checking means your code is evaluated before it runs to ensure it works as expected.

Thatā€™s also a limitation of TypeScript to keep in mind.

Runtime and Compile Time

TypeScript is JavaScriptā€™s runtime with a compile time type checker. ā€” TypeScript Handbook

  • Runtime is when JavaScript code gets executed
  • Compile time is when TypeScript code gets compiled to JavaScript code

TypeScript only checks your code at compile time.

This means you canā€™t rely on TypeScript for checks in your code such as user input when you ship your code.

example.ts
const pokemon = []

function addPokemon(pokemonName: string) {
  pokemon.push(pokemonName)
}

// ['Pikachu'] āœ…
addPokemon('Pikachu')

// Type 'number' not assignable to type 'string'. šŸš«
addPokemon(1)

Despite the TypeScript error, we can run the TypeScript code because type errors arenā€™t syntax errors.

example.js
const pokemon = []

function addPokemon(pokemonName) {
  pokemon.push(pokemonName)
}

// ['Pikachu'] āœ…
addPokemon('Pikachu')

// oops! šŸ’©
addPokemon(1)

This is the compiled (transpiled šŸ˜„) JavaScript code. TypeScript didnā€™t betray us. We neglected to put checks and error validation in the code.

example.ts
const pokemon = []

function addPokemon(pokemonName: string) {
  if (!pokemonName || typeof pokemonName !== 'string') {
    throw new Error('šŸ’© You have to specify a Pokemon name.')
  }

  pokemon.push(pokemonName)
}

// Type 'number' not assignable to type 'string'. šŸš«
addPokemon(1)

TypeScript doesnā€™t change how JavaScript works.

Gradual Adoption

So far weā€™ve seen we can reap the benefits of TypeScript without using TypeScript directly.

If youā€™re on the fence about TypeScript but prefer JSDoc you can take advantage of TypeScript being built-in. You can use JSDoc for types and have self-documenting code.

JSDoc

You can read more about JSDoc support for VS Code and look at the examples.

That being said we can adjust how strict type checking is when starting to use TypeScript so we donā€™t get overwhelmed making adding types a gradual adoption.

If you decide on using TypeScript you donā€™t have to rename everything at once but instead do it on a per-file basis.

TypeScript Introduction Summary

Letā€™s get a clear picture of TypeScript šŸ“ø:

  • TypeScript is a static type checker (TypeScript checks your code before you run it)
  • TypeScript is a superset of JavaScript (this means that any JavaScript program is also a valid TypeScript program)
  • TypeScript preserves the runtime behavior of JavaScript (TypeScript doesnā€™t change how JavaScript works)
  • TypesScript compiles to JavaScript
  • Types are gone once it compiles to JavaScript (the browser and Node donā€™t understand TypeScript)
  • Using TypeScript is a gradual adoption

JavaScript Types

ā€You donā€™t need TypeScript, we have TypeScript at homeā€.

True scholars šŸ§ among you with keen intellect might observe that JavaScript already has primitive types.

A primitive type is data that is not an object and has no methods.

JavaScript has 7 primitive types:

  • string (sequence of characters)
  • number (floating point is the only number type)
  • bigint (for huge numbers)
  • boolean (logical data type with two values)
  • undefined (assigned to variables that have just been declared)
  • symbol (unique values)
  • null (points to a nonexistent object or address)

Alongside those primitive types there are primitive wrapper objects:

  • String (for the string primitive)
  • Number (for the number primitive)
  • BigInt (for the bigint primitive)
  • Boolean (for the boolean primitive)
  • Symbol (for the symbol primitive)

Letā€™s clear up the difference between primitive types and primitive wrapper objects so you donā€™t get confused if you should use the lowercase string or capitalized String as a type.

example.js
// 'Pikachu'
const stringPrimitive = 'Pikachu'

// String { 'Pikachu' }
const stringObject = new String('Pikachu')

Primitives

JavaScript converts primitive types to primitive wrapper objects behind the scenes so we can use their methods.

This is because methods like toUpperCase extend the String object.

The reason we donā€™t use primitive wrapper objects is because itā€™s more work to get the value out of the object and we could get unexpected results if we pass an object to something expecting a primitive value.

That being said donā€™t confuse the new String constructor with the String function that does type conversion.

example.js
const number = '42'

const string = String(number) // '42'

As weā€™ve learned TypeScript doesnā€™t save us at runtime, so we have to put checks in place.

example.ts
function isString(value: string): boolean {
  return typeof value === 'string' ? true : false
}

isString('Pikachu') // true āœ…
isString(1) // false šŸš«

So why are we writing more code? Letā€™s look at an example of a addPokemon function.

example.js
const pokemon = []

function addPokemon(name, timeAdded) {
  pokemon.push({ name, timeAdded })
}

addPokemon('Pikachu', new Date())

Writing JavaScript we have to consider a lot of things:

  • Is the function callable?
  • Does the function return anything?
  • What are the arguments of the function?
  • What date format does the argument accept?

Even if we look at the implementation of addPokemon we donā€™t know what date format we should pass to timeAdded, so we turn to documentation.

TypeScript prevents us from making those mistakes in the first place by knowing we are accessing the right properties and passing the right arguments alongside code completion.

example.ts
const pokemon = []

function addPokemon(name: string, timeAdded: Date) {
  pokemon.push({ name, timeAdded })
}

In the example the argument name is of type string and argument timeAdded is of type Date which is just a built-in type.

Code completion

This is extremely useful when dealing with some API because the documentation lives inside your editor.

example.ts
// Type 'string' is not assignable to
// parameter of type 'Date'. šŸš«
addPokemon('Pikachu', Date())

Date string

The Date function returns a string but we have to pass the new Date constructor that returns an object. The type doesnā€™t match so TypeScript complains that you canā€™t assign string to Date.

example.ts
// [{ name: 'Pikachu', added: Date... }] āœ…
addPokemon('Pikachu', new Date())

Date constructor

Letā€™s start learning about TypeScript and using it in practice.

TypeScript Playground

To get started open the TypeScript Playground.

The playground is a great way of seeing the compiled JavaScript code and generated TypeScript types without having to set up anything while using the same Monaco editor that powers VS Code, so you should feel at home.

TypeScript Playground

The right side of the editor has some useful tabs:

  • .JS shows the compiled JavaScript output (the default target is ES2017 or EcmaScript which is the name of the JavaScript specification adjustable from the TS Config tab)
  • .D.TS has the generated TypeScript types
  • Errors is like the error logs in your console
  • Logs show the output of your code

You can press Ctrl + Enter to run the code in the playground. Another tip I have is to include console.clear() at the top so your logs stay readable.

If you canā€™t see the right sidebar press the arrow icon at the top right.

Type Inference

TypeScript can infer types to provide type information.

playground.ts
let pokemon = 'Pikachu'

pokemon = 'Charizard'

// 'CHARIZARD' āœ…
pokemon.toUpperCase()

// Type 'number' is not assignable to type 'string'. šŸš«
pokemon = 1

// This expression is not callable. šŸš«
pokemon()

How great is this instant feedback in your editor?

TypeScript can also infer the return type of a function. If it doesnā€™t return anything itā€™s void.

playground.ts
function returnPokemon() {
  // return string
  return 'Pikachu'
}

function logPokemon() {
  // we don't return anything
  console.log('Pikachu')
}

Itā€™s encouraged by the TypeScript documentation to let TypeScript infer the type when possible and later Iā€™m going to show an example of that but you can always be explicit if you want.

playground.ts
function returnPokemon(): string {
  // return string
  return 'Pikachu'
}

function logPokemon(): void {
  // we don't return anything
  console.log('Pikachu')
}

In the next example weā€™re looking at the return type of a fetch API request.

playground.ts
const API = 'https://pokeapi.co/api/v2/pokemon/'

async function getPokemon(name: string) {
  const response = await fetch(`${API}${name}`)
  const pokemon = await response.json()
  return pokemon
}

// shows Pikachu data from the Pokemon API
getPokemon('pikachu').then(console.log)

If you hover over getPokemon you can see TypeScript infered the return type as Promise<any>.

any is an escape hatch when we donā€™t know what the type is.

playground.ts
const API = 'https://pokeapi.co/api/v2/pokemon/'

async function getPokemon(
  name: string
): Promise<{ id: number, name: string }> {
  const response = await fetch(`${API}${name}`)
  const pokemon = await response.json()
  return pokemon
}

// shows Pikachu data from the Pokemon API
getPokemon('pikachu').then(console.log)

Here weā€™re more explicit about the return type with using the object type { id: number, name: string }.

I havenā€™t mentioned that we can do that because Promise<Type> is a generic but more on that later.

playground.ts
const API = 'https://pokeapi.co/api/v2/pokemon/'

async function getPokemon(
  name: string
): Promise<{ id: number, name: string }> {
  const response = await fetch(`${API}${name}`)
  const pokemon = await response.json()
  return pokemon
}

async function logPokemon(name: string) {
  const pokemon = await getPokemon(name)
  console.log({ id: pokemon.id, name: pokemon.name })
}

// { 'id': 25, 'name': 'pikachu' }
logPokemon('pikachu')

Thanks to this slight change we get code completion for the pokemon object since TypeScript knows the type. Itā€™s magic. šŸŖ„

Weā€™re going to go in-depth later and explore how to make this more organized by using other TypeScript features such as type alias and interface.

Type Annotations

Type annotations are an explicit way of specifying a type.

example.ts
const pokemon: string = 'Pikachu'

Primitive Types

TypeScript has the same basic primitive types.

example.ts
const pokemon: string = 'Pikachu'
const hp: number = 35
const caught: boolean = true

TypeScript Types

TypeScript extends the list of types:

  • any
  • unknown
  • void
  • never

any

Type any is a special type:

  • You can use type any as an escape hatch when you donā€™t want something to cause type checking errors
  • Type any represents all possible values
  • You get no type checking, so avoid using it
playground.ts
const apiResponse: any = {
  data: []
}

// we don't get any warning šŸ˜±
apiResponse.doesntExist

Using type any is useful in situations where:

  • Youā€™re working with a library that lacks types
  • You have a complex API response you donā€™t want to type
  • The API of the code youā€™re writing could change

If you want to focus on writing code I suggest instead of using any everywhere to include // @ts-nocheck at the top of your file. After youā€™re done writing code turn type checking back on. Donā€™t let your editor bully you.

Be conservative when using any because it defeats the purpose of using TypeScript.

unknown

Type unknown is the type-safe version of any:

  • You can assign any value to type unknown but you canā€™t do whatever you want
  • You must use checks to type narrow a value before you can use a it
  • You canā€™t access any object properties unless you type narrow first and then use type assertion
  • Type unknown can only be assigned to type unknown and type any
example.ts
const apiResponse: unknown = {
  data: []
}

// assignable to `any` āœ…
const anyType: any = apiResponse

// assignable to `unknown` āœ…
const unknownType: unknown = apiResponse

// 'unknown' not assignable to type '{ data: []; }'. šŸš«
const otherType: { data: [] } = apiResponse

// we have to use checks to narrow down the type
if (apiResponse && typeof apiResponse === 'object') {
  // Property 'data' does not exist on type 'object'. šŸš«
  apiResponse.data
}

We narrowed down what the type of apiResponse is, yet we canā€™t access the apiResponse.data property since itā€™s unknown.

To solve the problem we have to use type assertion that lets TypeScript know about the type.

playground.ts
if (apiResponse && typeof apiResponse === 'object') {
  const response = apiResponse as { data: [] }
  response.data // no warning āœ…
}

Weā€™re going to learn about type assertion later.

unknown is safer to use than any when we donā€™t know the function argument, so instead of being able to do anything inside prettyPrint we have to narrow the type of the input argument first to be able to use it.

playground.ts
function prettyPrint(input: unknown): string {
  if (Array.isArray(input)) {
    // we can run each value through prettyPrint again
    return input.map(prettyPrint).join(', ')
  }

  if (typeof input === 'string') {
    return input
  }

  if (typeof input === 'number') {
    return String(input)
  }

  return '...'
}

const values = ['Bulbasaur', 'Charmander', 'Squirtle', 1, {}]
const prettyValues = prettyPrint(values)

// 'Bulbasaur, Charmander, Squirtle, 1, ...'
console.log(prettyValues)

We can use unknown to describe a function that returns an unknown value. Because obj is unknown we have to narrow the type first before we do something reckless making it safe.

playground.ts
const json = '{ "id": 1, "name": "Pikachu" }'

function safeParse(value: string): unknown {
  return JSON.parse(value)
}

const obj = safeParse(json) // safe āœ…

If we didnā€™t use unknown the infered return type for safeParse would be any meaning you could do whatever you want with obj.

You should avoid using any or unknown if possible.

void

Type void is the absence of having any type.

Thereā€™s no point assigning void to a variable since only type undefined is assignable to type void.

example.ts
let pokemon: void

// only `undefined` is assignable to `void` āœ…
pokemon = undefined

// Type 'string' is not assignable to type 'void'. šŸš«
pokemon = 'Pikachu'

You mostly see type void used on functions that donā€™t return anything.

example.ts
function logPokemon(pokemon: string): void {
  console.log(pokemon)
}

logPokemon('Pikachu') // 'Pikachu'

Letā€™s learn how type void is useful when used in a forEach implementation.

playground.ts
function forEach(
  arr: any[],
  callback: (arg: any, index?: number) => void
): void {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i)
  }
}

forEach(
  ['Bulbasaur', 'Charmander', 'Squirtle'],
  (pokemon) => console.log(pokemon)
)

Because we use type void as the return type for forEach weā€™re saying the return value isnā€™t going to be used, so it can be called with a callback that returns any value.

Using the return type void explicity can save us from returning a value on accident during refactor.

playground.ts
function logPokemon(pokemonList: string[]): void {
  pokemonList.forEach(pokemon => {
    // ...
    return pokemon
  })
}

function logPokemonRefactor(pokemonList: string[]): void {
  for (const pokemon of pokemonList) {
    // ...
    // Type 'string' is not assignable to type 'void'. šŸš«
    return pokemon
  }
}

never

Type never represents values that never occur:

  • Type never canā€™t have a value
  • You use type never when thereā€™s no reachable end point like a while loop or error exception
  • Variables get the type never when narrowed by type guards to remove possibilities (a great example is preventing impossible states when a prop is passed to a component where you can say if one type of prop gets passed another canā€™t)
playground.ts
function infiniteLoop(): never {
  while (true) {
    // ...
  }
}

function error(message: string): never {
  throw new Error(message)
}

function timeout(ms: number): Promise<never> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('āŒ› Timed out.')), ms)
  })
}

Type unknown can be used together with type narrowing to ensure we have a check for each Pokemon type.

playground.ts
function getPokemonByType(
  pokemonType: 'fire' | 'water' | 'electric'
) {
  // type is 'fire' | 'water' | 'electric'
  if (pokemonType === 'fire') {
    return 'šŸ”„ Fire Pokemon'
  }

  // we narrow it down to 'water' | 'electric'
  if (pokemonType === 'water') {
    return 'šŸŒ€ Water Pokemon'
  }

  // only 'electric' is left
  pokemonType

  // remainingPokemonTypes can't have any value
  // because pokemonType is 'electric' šŸš«
  const remainingPokemonTypes: never = pokemonType

  return remainingPokemonTypes
}

getPokemonByType('electric')

Because of this check we know that getPokemonByType is missing a check for the electric type.

We havenā€™t yet learned about some of the types in the examples. Weā€™re not glossing over them. Weā€™re going to cover each later.

Array Types

Thereā€™s two equivalent ways to specify an array type in TypeScript.

To specify an array type you can use the generics Array<Type> syntax or the Type[] syntax.

playground.ts
const pokemon1: Array<string> = ['Bulbasaur', 'Charmander', 'Squirtle']

const pokemon2: string[] = ['Bulbasaur', 'Charmander', 'Squirtle']

I prefer the Type[] syntax because itā€™s less to type.

Function Types

You can specify the input and output type of functions.

playground.ts
const pokemon: string[] = []

function addPokemon(name: string): string[] {
  pokemon.push(name)
  return pokemon
}

addPokemon('Pikachu')

We explicitly typed pokemon as string[]. If TypeScript canā€™t infer the type it would use any by default, or in this case the return type of addPokemon would be any[].

Anonymous functions besides using contextual typing arenā€™t any different and using type annotations is the same.

playground.ts
const pokemon: string[] = []

const addPokemon = (name: string): string[] => {
    pokemon.push(name)
    return pokemon
}

addPokemon('Pikachu')

TypeScript uses contextual typing to figure out the type of the argument based on how itā€™s used inside an anonymous function.

playground.ts
const pokemonList = ['Bulbasaur', 'Charmander', 'Squirtle']

// 'Bulbasaur', 'Charmander', 'Squirtle'
pokemonList.forEach(pokemon => console.log(pokemon))

TypeScript infered pokemonList is of type string[]. Because of this inside forEach it knows the individual pokemon type should be of type string.

playground.ts
// being explicit āœ…
const pokemonList: string[] = ['Bulbasaur', 'Charmander', 'Squirtle']

// this is redundant šŸš«
pokemonList.forEach((pokemon: string) => console.log(pokemon))

Knowing about contextual typing you understand what things you donā€™t have to type.

Your goal isnā€™t to please TypeScript but use it to give you confidence your code works as expected.

Function arguments can be made optional by using the ? operator.

playground.ts
function logPokemon(name: string, hp?: number) {
  console.log({ name, hp })
}

// { name: 'Pikachu', hp: undefined } āœ…
logPokemon('Pikachu')

// { name: 'Pikachu', hp: 35 } āœ…
logPokemon('Pikachu', 35)

Function Overloads

Function overloading is the ability to create multiple functions of the same name with different implementations. Which implementation gets used depends on the arguments you pass in.

In JavaScript there is no function overloading because we can pass any number of parameters of any type we then perform checks on inside the function.

example.js
function logPokemon(arg1, arg2) {
  if (typeof arg1 === 'string' && typeof arg2 === 'number') {
    console.log(`${arg1} has ${arg2} HP.`)
  }

  if (typeof arg1 === 'object') {
    const { name, hp } = arg1
    console.log(`${name} has ${hp} HP.`)
  }
}

// 'Pikachu has 35 HP.' āœ…
logPokemon('Pikachu', 35)

// 'Pikachu has 35 HP.' āœ…
logPokemon({ name: 'Pikachu', hp: 35 })

TypeScript has overload signatures that let you call a function in different ways.

playground.ts
interface Pokemon {
  name: string
  hp: number
}

function logPokemon(name: string, hp: number): void
function logPokemon(pokemonObject: Pokemon): void

function logPokemon(arg1: unknown, arg2?: unknown): void {
  // matches the first overload signature
  if (typeof arg1 === 'string' && typeof arg2 === 'number') {
    // arg1 is `name` and arg2 is `hp`
    console.log(`${arg1} has ${arg2} HP.`)
  }

  // matches the second overload signature
  if (typeof arg1 === 'object') {
    // since it's an object we can assert the type to be Pokemon
    const { name, hp } = arg1 as Pokemon
    // log the destructured values
    console.log(`${name} has ${hp} HP.`)
  }
}

// 'Pikachu has 35 HP.' āœ…
logPokemon('Pikachu', 35)

// 'Pikachu has 35 HP.' āœ…
logPokemon({ name: 'Pikachu', hp: 35 })

Letā€™s break it down into steps:

  • We wrote two overload signatures for logPokemon
  • The first overload signature accepts name and hp arguments
  • The second overload signature accepts a pokemonObject argument
  • After that we wrote a function implementation with a compatible signature where the second arg2 argument is optional since the minimal amount of arguments is one
  • If arg1 is string and arg2 is number we know it matches the first signature
  • If arg1 is an object we know it matches the second signature, so we can use type assertion and destructure the name and hp values from it

Overload signature

Overload signature

Object Types

The object type is like a regular object in JavaScript.

If you hover over pokemonInferedType TypeScript already knows itā€™s shape.

playground.ts
// infered type
const pokemonInferedType = {
  name: 'Pikachu'
}

// explicit type
const pokemonExplicitType: { name: string } = {
  name: 'Pikachu'
}

In the next examples we can see how TypeScript treats missing, optional, and extra object properties.

playground.ts
// Property 'id' is missing in type '{ name: string; }' but
// required in type '{ id: number; name: string; }'. šŸš«
const pokemonMissingProperty: { id: number, name: string } = {
  name: 'Pikachu'
}

// `id` is optional āœ…
const pokemonOptionalArgument: { id?: number, name: string } = {
  name: 'Pikachu'
}

// Object literal may only specify known properties. šŸš«
const pokemonExtraProperty: { id?: number, name: string } = {
  id: 1,
  name: 'Pikachu',
  pokemonType: 'electric'
}

Weā€™re going to learn ways to abstract types to make them more readable and reusable next.

Type Aliases

So far weā€™ve been using types directly using type annotations. This is hard to read and not reusable.

A type alias is as the name suggests ā€” just an alias for a type.

Just how we assign names to different types šŸ¤­ of people.

You should already be familiar with the object type syntax.

playground.ts
type Pokemon = { id: number, name: string, pokemonType: string }

const pokemon: Pokemon[] = [{
  id: 1,
  name: 'Pikachu',
  pokemonType: 'electric'
}]

// { 'id': 1, 'name': 'Pikachu', 'pokemonType': 'electric' }
pokemon.forEach(pokemon => console.log(pokemon))

Instead of writing Pokemon[] where [] indicates to TypeScript itā€™s an array of Pokemon we can say { id: number, name: string, type: string }[] which is equivalent.

playground.ts
type Pokemon = { id: number, name: string, pokemonType: string }[]

const pokemon: Pokemon = [{
  id: 1,
  name: 'Pikachu',
  pokemonType: 'electric'
}]

// { 'id': 1, 'name': 'Pikachu', 'pokemonType': 'electric' }
pokemon.forEach(pokemon => console.log(pokemon))

I prefer the Pokemon[] syntax because of reusability. We can use the Pokemon type on single Pokemon where it makes sense and Pokemon[] on a collection of Pokemon.

This saves us from creating another type and using weird grammar like Pokemons to indicate thereā€™s many despite Pokemon already being plural.

The next example shows how we can reuse the Pokemon type by using it as the argument and return type of the logPokemon function.

playground.ts
type Pokemon = string[] | string

function logPokemon(pokemon: Pokemon): Pokemon {
  if (Array.isArray(pokemon)) {
    return pokemon.map(pokemon => pokemon.toUpperCase())
  }

  if (typeof pokemon === 'string') {
    return pokemon.toUpperCase()
  }

  return 'Please enter a Pokemon.'
}

// ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE']
console.log(logPokemon(['Bulbasaur', 'Charmander', 'Squirtle']))

// 'PIKACHU'
console.log(logPokemon('Pikachu'))

// 'Please enter a Pokemon.'
console.log(logPokemon())

Functions are just special objects in JavaScript which is a roundabout way of saying we can type them as any other object.

Functions are objects

The next examples shows ways to type different function expressions using a type alias.

playground.ts
type LogPokemon = (pokemon: string) => void

// named function expression
const logPokemon1: LogPokemon = function logPokemon(pokemon) {
  console.log(pokemon)
}

// anonymous function expression
const logPokemon2: LogPokemon = function(pokemon) {
  console.log(pokemon)
}

// anonymous arrow function expression
const logPokemon3: LogPokemon = (pokemon) => console.log(pokemon)

The next example shows how we can use a construct signature inside a type alias to type a constructor function.

playground.ts
type Pokemon = {
  name: string
  pokemonType: string
}

type PokemonConstructor = {
  new(name: string, pokemonType: string): Pokemon
}

class PokemonFactory implements Pokemon {
  name: string
  pokemonType: string

  constructor(name: string, pokemonType: string) {
    this.name = name
    this.pokemonType = pokemonType
  }
}

function addPokemon(
  pokemonConstructor: PokemonConstructor,
  name: string,
  pokemonType: string
): Pokemon {
  return new pokemonConstructor(name, pokemonType);
}

const pokemon: Pokemon = addPokemon(
  PokemonFactory,
  'Pikachu',
  'electric'
)

// PokemonFactory: { "name": "Pikachu", "pokemonType": "electric" }
console.log(pokemon)

Confusing, right? This is just showing you itā€™s possible, so donā€™t think about it.

Interfaces

Interfaces are another way to name an object type.

playground.ts
interface Pokemon {
  id?: number
  name: string
  pokemonType: string
  ability: string
  attack(): void
}

const pokemon: Pokemon[] = [{
  id: 1,
  name: 'Pikachu',
  pokemonType: 'electric',
  ability: 'āš” Thunderbolt',
  attack() {
    console.log(`${this.name} used ${this.ability}.`)
  }
}]

// 'Pikachu used āš” Thunderbolt'
pokemon.forEach(pokemon => pokemon.attack())

Inside the interface we can say properties are optional using ? and type function signatures like attack(): void.

Interfaces and type aliases are almost interchangable.

You can also use a semicolon or period after each property if you want as itā€™s purely optional.

Type Aliases or Interfaces?

For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use interface until you need to use features from type. - TypeScript Handbook

The short answer is ā€” doesnā€™t matter. Pick one and if it doesnā€™t work for you use the other one.

I use both when it makes sense:

  • Interface is more appropriate for describing shapes of objects
  • You can add new fields to an existing interface but not to a type alias

We havenā€™t learned about intersections yet but briefly it just lets us combine types.

The next example shows how using intersections we can extend a type alias by combining the type Pokemon and { pokemonType: 'electric' } into a new type Electric.

playground.ts
type Pokemon = {
  id: number
  name: string
}

type Electric = Pokemon & { pokemonType: 'electric' }

// has to satisfy the same checks āœ…
const pikachu: Electric = {
  id: 1,
  name: 'Pikachu',
  pokemonType: 'electric'
}

In the case of interfaces we use the extends keyword to extend them.

playground.ts
interface Pokemon {
  id: number
  name: string
}

interface Electric extends Pokemon {
  pokemonType: 'electric'
}

// has to satisfy the same checks āœ…
const pikachu: Electric = {
  id: 1,
  name: 'Pikachu',
  pokemonType: 'electric'
}

When using an interface you should always keep in mind that you can use an existing interface which could lead to some unexpected results.

playground.ts
interface Window {
  color: string
  style: 'double-hung' | 'casement' | 'awning' | 'slider'
}

// Type '{ color: string; style: "double-hung"; }' is missing
// the following properties from type 'Window': applicationCache,
// clientInformation, closed, customElements, and 207 more. šŸš«
const item: Window = {
  color: 'white',
  style: 'double-hung'
}

The type Window is already declared in lib.dom.d.ts types for the global window object. šŸ’©

Letā€™s briefly touch upon naming conventions. For the most part I just name the type the capitalized version of what Iā€™m trying to type. For example pokemon would be Pokemon.

Sometimes that wonā€™t work like in the case of React where component names are capitalized so you can use a suffix such as PokemonProps.

For whatever reason using the prefix IPokemon for an interface is controversial inside TypeScript circles but you probably shouldnā€™t care as long as your naming convention is consistent.

Union Types

A union type is a type made from at least two types and represents any values of those types.

playground.ts
function logPokemon(pokemon: string[] | string) {
  console.log(pokemon)
}

// ['Bulbasaur', 'Charmander', 'Squirtle'] āœ…
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

// Pikachu āœ…
logPokemon('Pikachu')

The pokemon argument can only be an array of Pokemon of type string[] or a single Pokemon of type string.

You can only do things with the union type that every member supports meaning that you canā€™t just use a string method without checks because you said to TypeScript the type could either be string[] or string.

playground.ts
function logPokemon(pokemon: string[] | string) {
  // Property 'toUpperCase' does not exist on type 'string[]'. šŸš«
  console.log(pokemon.toUpperCase())
}

logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])
logPokemon('Pikachu')

Instead we have to put checks in place to narrow down the type so TypeScript knows the exact type.

playground.ts
function logPokemon(pokemon: string[] | string) {
  if (Array.isArray(pokemon)) {
    // `pokemon` can only be an array āœ…
    console.log(pokemon.map(pokemon => pokemon.toUpperCase()))
  }

  if (typeof pokemon === 'string') {
    // `pokemon` can only be string āœ…
    console.log(pokemon.toUpperCase())
  }
}

// ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE']
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

// PIKACHU
logPokemon('Pikachu')

Unfortunately the second if...else statement is required because TypeScript canā€™t narrow down the pokemon type to string.

In situations where the union members like string[] and string overlap and share the same methods such as slice you donā€™t have to narrow the type.

playground.ts
function logPokemon(pokemon: string[] | string) {
  // works for both types āœ…
  console.log(pokemon.slice(0, 1))
}

// ['Bulbasaur']
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

// 'P'
logPokemon('Pikachu')

Discriminated Unions

A discriminated union is the result of narrowing the members of the union that have the same literal type.

For example we can use a type guard to narrow the Pokemon type 'fire' | 'water' to 'fire' or 'water'.

The next example shows what you might expect would work but doesnā€™t because TypeScript canā€™t determine if the property exists.

Letā€™s switch šŸ¤­ it up for fun.

playground.ts
interface Pokemon {
  flamethrower?: () => void
  whirlpool?: () => void
  pokemonType: 'fire' | 'water'
}

function pokemonAttack(pokemon: Pokemon) {
  switch (pokemon.pokemonType) {
    case 'fire':
      // Cannot invoke an object which is possibly 'undefined'. šŸš«
      pokemon.flamethrower()
      break
    case 'water':
      // Cannot invoke an object which is possibly 'undefined'. šŸš«
      pokemon.whirlpool()
      break
  }
}

// šŸ”„ 'Flamethrower'
pokemonAttack({
  pokemonType: 'fire',
  flamethrower: () => console.log('šŸ”„ Flamethrower')
})

// šŸŒ€ 'Whirlpool'
pokemonAttack({
  pokemonType: 'water',
  whirlpool: () => console.log('šŸŒ€ Whirlpool')
})

The type-checker canā€™t determine if flamethrower or whirpool is present based on the pokemonType property because they could not exist as theyā€™re both optional.

To solve this problem we have to be more explicit and separate the arguments so TypeScript can be sure of the type.

playground.ts
interface Fire {
  flamethrower: () => void
  pokemonType: 'fire'
}

interface Water {
  whirlpool: () => void
  pokemonType: 'water'
}

type Pokemon = Fire | Water

function pokemonAttack(pokemon: Pokemon) {
  switch (pokemon.pokemonType) {
    case 'fire':
      pokemon.flamethrower() // āœ…
      break
    case 'water':
      pokemon.whirlpool() // āœ…
      break
  }
}

// šŸ”„ 'Flamethrower'
pokemonAttack({
  pokemonType: 'fire',
  flamethrower: () => console.log('šŸ”„ Flamethrower')
})

// šŸŒ€ 'Whirlpool'
pokemonAttack({
  pokemonType: 'water',
  whirlpool: () => console.log('šŸŒ€ Whirlpool')
})

By the use of a type guard style check ==, ===, !=, !== or switch on the discriminant property pokemonType TypeScript can do type narrowing based on the literal type.

This also helps us catch mistakes if something passed through the switch statement that shouldnā€™t have.

Intersection Types

Intersection types let us combine types using the & operator.

The next example also shows how we can use interfaces and type aliases together.

playground.ts
interface Pokemon {
  name: string
  hp: number
  pokemonType: [string, string?]
}

interface Ability {
  blaze(): void
}

interface Moves {
  firePunch(): void
}

type Fire = Ability & Moves

type FirePokemon = Pokemon & Fire

const charizard: FirePokemon = {
  name: 'Charizard',
  hp: 100,
  pokemonType: ['fire', 'flying'],
  blaze() {
    console.log(`${this.name} used šŸ”„ Blaze.`)
  },
  firePunch() {
    console.log(`${this.name} used šŸ”„ Fire Punch.`)
  }
}


charizard.blaze() // 'Charizard used šŸ”„ Blaze.'
charizard.firePunch() // 'Charizard used šŸ”„ Fire Punch.'

I threw in a sneaky tuple in pokemonType because a Pokemon can have dual-types. Weā€™re going to learn more about tuple later.

If we wanted to use an interface we could and it works just the same.

example.ts
interface Fire extends Ability, Moves {}

interface FirePokemon extends Pokemon, Fire {}

Itā€™s easier to compose types using type aliases.

Type Assertion

Type assertion is like type casting in TypeScript where it can be used to specify or override another type.

In this example TypeScript only knows that formEl returns some kind of HTMLElement or null from document.getElementById meaning we canā€™t use some of the built-in form methods.

playground.ts
// formEl is `HTMLElement | null` because it might not exist
const formEl = document.getElementById('form')

These types correspond to the browser API. Element is the base class other elements inherit from. HTMLElement is the base interface for HTML elements. Thatā€™s how we get to the HTMLFormElement that represents a <form> element in the DOM (Document Object Model).

playground.ts
const formEl = document.getElementById('form')

// Property 'reset' does not exist on type 'HTMLElement'. šŸš«
formEl?.reset()

If youā€™re not familiar with the optional chaining operator it just says to TypeScript the value canā€™t be null.

example.js
// short-circuit evaluation
formEl && formEl.reset()

// optional chaining equivalent
formEl?.reset()

Itā€™s a relatively recent addition to JavaScript but itā€™s been a part of TypeScript for a while, so you might not be familiar with it. If the value doesnā€™t exist itā€™s just going to return undefined.

If we look at the API for HTMLElement the TypeScript error makes complete sense since that method doesnā€™t exist.

We can be more specific about the type of element with type assertion using the as keyword.

playground.ts
const formEl = document.getElementById('form') as HTMLFormElement

formEl?.reset() // works āœ…

The šŸ§  pro-tip for how to figure out what element you want is fumbling around until your code completion gives you an option that looks like what you want.

Thereā€™s also the alternative angle bracket <> syntax for type assertion.

playground.ts
const formEl = <HTMLFormElement>document.getElementById('form')

formEl?.reset() // works āœ…

Donā€™t confuse this syntax with TypeScript generics.

The angle bracket syntax is avoided because it gets mistaken for React components because of JSX.

We can observe this error in the TypeScript Playground because in the TS Config the JSX for React option is on by default.

playground.ts
// JSX element 'HTMLFormElement' has no corresponding closing tag. šŸš«
const formEl = <HTMLFormElement>document.getElementById('form')

Event listeners are a big part of JavaScript and something we use often even in JavaScript frameworks.

In the next example we have an input field with an event listener that takes a Pokemon name.

example.html
<input type="text" id="pokemon" />
playground.ts
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement

function handleInput(event) {
  // ...
}

pokemonInputEl.addEventListener('input', (event) => handleInput(event))

When you hover over event in addEventListener notice the infered type Event.

This makes sense because Event is the base interface for events. Derived from Event thereā€™s UIEvent that has other interfaces like MouseEvent, TouchEvent and KeyboardEvent among others.

This is a teachable moment that lets us know we can leverage TypeScript to help us figure out built-in types instead of having to look it up.

playground.ts
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement

function handleInput(event: Event) {
  // Object is possibly 'null'. šŸš«
  // Property 'value' does not exist on type 'EventTarget'. šŸš«
  event.target.value
}

pokemonInputEl.addEventListener('input', (event) => handleInput(event))

The problem we face has to do again with the type not being specific enough. We want the value from event.target but we can see the type is EventTarget so TypeScript doesnā€™t let us access that property.

playground.ts
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement

function handleInput(event: Event) {
  const targetEl = event.target! as HTMLInputElement
  targetEl.value
}

pokemonInputEl.addEventListener('input', (event) => handleInput(event))

TypeScript knows the type is HTMLInputElement, so we can use itā€™s methods and properties.

You donā€™t have to keep this knowledge in your head. Let TypeScript and your editor help you figure out what type to use.

playground.ts
// event is `MouseEvent` šŸ­
window.addEventListener('mouseover', (event) => console.log('Mouse event'))

// event is `TouchEvent` šŸ‘†
window.addEventListener('touchmove', (event) => console.log('Touch event'))

// event is `KeyboardEvent` šŸŽ¹
window.addEventListener('keyup', (event) => console.log('Keyboard event'))

In the previous example we used ! that asserts the type canā€™t be null but keep in mind that when using any type assertion youā€™re saying the element canā€™t be null.

playground.ts
// HTMLElement | null
const pokemonInputEl = document.getElementById('pokemon')

// HTMLInputElement
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement

That being said donā€™t lie to the TypeScript compiler and only use type assertion when you have to.

Type Assertion Using !

Using the ! syntax after any expression is a type assertion that the value isnā€™t going to be null or undefined.

playground.ts
const formEl = document.getElementById('form')! as HTMLFormElement

formEl.reset() // works āœ…

Type Assertion Conversion

Using any and type assertion is great when youā€™re just writing code but avoid using them unless you have to.

TypeScript tries itā€™s best to not let you do something stupid.

playground.ts
// Conversion of type 'string' to type 'number' may be a mistake because
// neither type sufficiently overlaps with the other. If this was
// intentional, convert the expression to 'unknown' first. šŸš«
const pokemon = 'Pikachu' as number

That doesnā€™t mean you canā€™t do whatever you want.

playground.ts
const pokemon = 'Pikachu' as unknown as number

// looks great šŸ’„
pokemon.toFixed()

TypeScript only allows type assertions that convert to a more specific or less specific version of a type.

Literal Types

Literal types are exact values of strings and numbers.

Both var and let allow for changing what is held inside the variable, and const does not. This is reflected in how TypeScript creates types for literals. ā€” TypeScript Handbook

TypeScript has string, number, and boolean literals. The boolean type itself is just an alias for the true | false union.

playground.ts
// type is string
let pokemonGeneralType = 'Pikachu'

// type is 'Pikachu'
const pokemonLiteralType = 'Pikachu'

This concept is more useful if we combine type literals into unions.

playground.ts
function movePokemon(
  direction: 'up' | 'right' | 'down' | 'left'
) {
  // ...
}

movePokemon('up') // acceptable value āœ…
movePokemon('rigth') // oops! typo. šŸš«

Literal Inference

Literal inference is when TypeScript thinks properties on an object might change, so instead of infering a literal type it infers a primitive type.

Types are used to determine reading and writing behavior.

playground.ts
function addPokemon(
  name: string,
  pokemonType: 'šŸ”„ fire' | 'šŸŒ€ water' | 'āš” electric'
) {
  console.log({ name, pokemonType })
}

const pokemon = {
  name: 'Pikachu',
  pokemonType: 'āš” electric'
}

// Argument of type 'string' is not assignable to
// parameter of type '"šŸ”„ fire" | "šŸŒ€ water" | "āš” electric"'. šŸš«

// `pokemonType` is 'string' instead of 'āš” electric' šŸš«
addPokemon(pokemon.name, pokemon.pokemonType)

We can use type assertion on pokemonType.

playground.ts
const pokemon = {
  name: 'Pikachu',
  pokemonType: 'āš” electric' as 'āš” electric'
}

The easier method is using the as const suffix that acts like the type equivalent of const.

playground.ts
const pokemon = {
  name: 'Pikachu',
  pokemonType: 'āš” electric'
} as const

TypeScript adds a readonly type on the pokemon properties that signals they wonā€™t change, so TypeScript knows itā€™s a literal type instead of a general type like string or number.

example.ts
const pokemon: {
  readonly name: "Pikachu"
  readonly pokemonType: "electric"
}

This signals to TypeScript the values of pokemon wonā€™t change.

Object Index Signatures

When we donā€™t know the properties of an object ahead of time but know the shape of the values we can use index signatures.

JavaScript has two ways to access a property on an object:

  • The dot operator: object.property
  • Square brackets: object['property']

The second syntax is called index accessors.

This is reflected in the types system where you can add an index signature to unknown properties.

playground.ts
interface PokemonAPIResponse {
  [index: string]: unknown
}

Pokemon API

This shows the Pokemon API response.

Weā€™re using an index signature syntax [key: type]: type to indicate that the keys of the object are going to be a string with an unknown property.

This is useful when we donā€™t have control over an API or time to type out a complex interface.

In the example we might only care about some properties and not the rest.

playground.ts
interface PokemonAPIResponse {
  // let any other property through that
  // matches the index signature
  [index: string]: unknown
  base_experience: number
  id: number
  name: string
  abilities: Array<{ ability: { name: string, url: string }}>
  moves: Array<{ move: { name: string, url: string }}>
}

const API = 'https://pokeapi.co/api/v2/pokemon/'

async function getPokemon(
  name: string
): Promise<PokemonAPIResponse> {
  const response = await fetch(`${API}${name}`)
  const pokemon = await response.json()
  return pokemon
}

async function logPokemon(name) {
  const pokemon = await getPokemon(name)

  // Object is of type 'unknown'. šŸš«
  console.log(pokemon.order.toString())

  // we need checks in place for properties we don't know about
  console.log((pokemon.order as number).toString())

  // we get great code completion for properties we know about
  pokemon.abilities.forEach(
    ({ ability }) => console.log(ability.name)
  )
}

logPokemon('pikachu') // '35', 'static', 'lightning-rod'

Hereā€™s another example where we have Pokemon ratings but donā€™t know all the Pokemon ahead of time.

playground.ts
type Rating = 1 | 2 | 3 | 4 | 5

interface PokemonRatings {
  [pokemon: string]: Rating
  bulbasaur: Rating
  charmander: Rating
  squirtle: Rating
}

This is a great opportunity to explain something that might confuse you when dealing with dynamic code where youā€™re accesing object properties such as object[key] where key is dynamic.

playground.ts
interface Stats {
  id: number
  hp: number
  attack: number
  defense: number
}

interface Pokemon {
  bulbasaur: Stats
  charmander: Stats
  squirtle: Stats
}

const pokemon: Pokemon = {
  bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
  charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
  squirtle: { id: 3, hp: 44, attack: 48, defense: 65 }
}

const bulbasaur: string = 'bulbasaur'

// No index signature with a parameter of type 'string'
// was found on type 'Pokemon'. šŸš«
pokemon[bulbasaur]

TypeScript thinks weā€™re trying to access the object property by using a string index when objects are number indexed by default.

In reality we want to access the pokemon object using the type literal bulbasaur instead of string.

We can see this is true if we change the index signature to string meaning we can pass anything that matches that index signature.

playground.ts
interface Pokemon {
  [pokemon: string]: Stats
  bulbasaur: Stats
  charmander: Stats
  squirtle: Stats
}

// careful if this is what you want āš ļø
// general type 'string' āœ…
pokemon[bulbasaur]

What you should do is make sure youā€™re passing a type literal instead or use the as const assertion. Iā€™m only explicit here so itā€™s obvious but using const already does that.

playground.ts
const bulbasaur: 'bulbasaur' = 'bulbasaur'

// string literal 'bulbasaur' āœ…
pokemon[bulbasaur]

Type Narrowing

Type narrowing is when you narrow types to more specific types, so you can limit what you can do with a certain value.

playground.ts
function getPokemonByType(
  pokemonType: 'fire' | 'water' | 'electric'
) {
  // type is 'fire' | 'water' | 'electric'
  if (pokemonType === 'fire') {
      return 'šŸ”„ Fire type Pokemon'
  }

  // we narrow it down to 'water' | 'electric'
  if (pokemonType === 'water') {
      return 'šŸŒ€ Water type Pokemon'
  }

  // we narrow it down to 'electric'
  pokemonType

  // TypeScript knows only 'electric' is possible
  return 'āš” Electric type Pokemon'
}

getPokemonByType('electric')

Letā€™s appreciate for a moment how cool it is that TypeScript just knows the type weā€™re dealing with based on analyzing the code flow. šŸ¤Æ

The next example shows type narrowing using the in operator that checks if a property is in the object.

playground.ts
type Fire = { flamethrower: () => void }

type Water = { whirlpool: () => void }

type Electric = { thunderbolt: () => void }

function pokemonAttack(pokemon: Fire | Water | Electric) {
  if ('flamethrower' in pokemon) {
    pokemon.flamethrower()
  }

  if ('whirlpool' in pokemon) {
    pokemon.whirlpool()
  }

  if ('thunderbolt' in pokemon) {
    pokemon.thunderbolt()
  }
}

const pokemon = {
  name: 'Pikachu',
  thunderbolt() {
    console.log(`${this.name} used āš” Thunderbolt.`)
  }
}

pokemonAttack(pokemon) // 'Pikachu used āš” Thunderbolt.'

If you remember from a previous example thereā€™s a distinction between the constructor new Date() that returns an object and function Date() that returns a string.

In the same way we can use instanceof to check if a value is an instance of another value.

playground.ts
function logDate(date: Date | string) {
  if (date instanceof Date) {
    console.log(date.toUTCString())
  } else {
    console.log(date.toUpperCase())
  }
}

// 'THU JUL 01 2021 20:00:00 GMT+0200 (CENTRAL EUROPEAN SUMMER TIME)'
logDate(Date())

// 'Thu, 01 Jul 2021 20:00:00 GMT'
logDate(new Date())

You can read more about narrowing that covers some plain JavaScript concepts if youā€™re not familiar with concepts such as truthiness and equality checks.

Type Guards

A type guard is a check against the value returned by typeof.

Because TypeScript often knows more than us about some intricacies of JavaScript it can save us from some JavaScript quirks.

playground.ts
function logPokemon(pokemon: string[] | null) {
  if (typeof pokemon === 'object') {
    // Object is possibly 'null'. šŸš«
    pokemon.forEach(pokemon => console.log(pokemon))
  }

  return pokemon
}

logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

TypeScript lets us know the pokemon type was narrowed down to string[] | null instead of just string[].

In logPokemon we naively check if pokemon is an array by checking if itā€™s an object.

This isnā€™t a bad assumption since almost everything in JavaScript is an object. We donā€™t know one thing ā€” null is also an object.

In JavaScript null is a primitive value that returns object.

example.js
console.log(typeof null) // 'object'

This mistake is part of the JavaScript language. You can read The history of ā€œtypeof nullā€ to learn why that is so.

Type Predicates

Type predicates are a special return type thatā€™s like a type guard for functions.

Take for example a Pokemon list where not every item is a Pokemon. We can use the isPokemon function to check if a value is a Pokemon. In filterPokemon the pokemonList array is of type unknown[] so pokemon is unknown.

We expect TypeScript to narrow the type through the if...else statement to make sure the item weā€™re passing is a Pokemon, so we can access itā€™s properties but pokemon is unknown.

playground.ts
interface Pokemon {
  name: string
  itemType: string
}

const pokemonList: Pokemon[] = [
  {
    name: 'Pikachu',
    itemType: 'pokemon',
  },
  {
    name: 'Berry',
    itemType: 'consumable'
  }
]

function isPokemon(value: any): boolean {
  return value.itemType === 'pokemon' ? true : false
}

function filterPokemon(pokemonList: unknown[]) {
  return pokemonList.filter(pokemon => {
    if (isPokemon(pokemon)) {
      // Object is of type 'unknown'. šŸš«
      console.log(pokemon.name)
    }
  })
}

filterPokemon(pokemonList) // 'Pikachu'

How do we narrow the type of pokemon to be Pokemon?

We can use type predicates to narrow the pokemon type to Pokemon by using the argumentName is Type syntax that changes the argument type if the function returns true.

playground.ts
function isPokemon(value: any): value is Pokemon {
  return value.itemType === 'pokemon' ? true : false
}

The pokemon type is narrowed to Pokemon and we can access itā€™s properties.

Generics

Generics are variables for types.

We use generics to create reusable pieces of code that can work over a variety of types rather than a single one.

The first generic we have encountered was the array type.

playground.ts
const pokemon: Array<string> = ['Bulbasaur', 'Charmander', 'Squirtle']

The Array type is using an interface Array<T> where T represents a type variable. That means we can pass our own types.

playground.ts
const pokemon: Array<{ name: string, pokemonType: string }> = [{ name: 'Pikachu', pokemonType: 'electric' }]

To make the code more readable we can use a type alias or interface.

playground.ts
interface Pokemon {
  name: string
  pokemonType: string
}

const pokemon: Array<Pokemon> = [{ name: 'Pikachu', pokemonType: 'electric' }]

If you remember what we learned before, instead of the Array<Pokemon> syntax we can use the equivalent Pokemon[] syntax.

Letā€™s look at an example where we use logPokemon to log Pokemon where we donā€™t know the type ahead of time because a user could pass anything from a string to an array.

playground.ts
function logPokemon(pokemon: any) {
  return pokemon
}

const log1 = logPokemon('Pikachu')
const log2 = logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

Hovering over the values we can see the infered type for logPokemon is (pokemon: any): any and log1 and log2 types are any.

We might think of using a union type to handle the types but itā€™s not ideal since it wouldnā€™t narrow the type without type guards.

playground.ts
function logPokemon(pokemon: string | string[]) {
  return pokemon
}

const log1 = logPokemon('Pikachu')
const log2 = logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

// Property 'toUpperCase' does not exist on type 'string[]'. šŸš«
log1.toUpperCase()

// Property 'map' does not exist on type 'string'. šŸš«
log2.map(pokemon => pokemon.toUpperCase)

Hovering over logPokemon the call signature type is (pokemon: string | string[]): string | string[] and the type for log1 and log2 is string | string[].

It would be easier to let the user pass in their own type using generics.

The generics syntax is <Type> where Type represents the type variable. You might see the single letter T used instead but I think itā€™s confusing. It makes the code harder to read and you might assume you have to use T as a type name. You can name the type variable anything you want.

playground.ts
function logPokemon<Type>(pokemon: Type): Type {
  return pokemon
}

const log1 = logPokemon('Pikachu')
const log2 = logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

// string āœ…
log1.toUpperCase()

// string of arrays āœ…
log2.map(pokemon => pokemon.toUpperCase)

In the above example weā€™re saying the logPokemon is a generic function that takes a type parameter Type and an argument pokemon of Type and returns Type.

Hovering over logPokemon for log1 we can see the call signature matches the string we passed in logPokemon<"Pikachu">(pokemon: "Pikachu"): "Pikachu" and log1 is the type literal Pikachu.

Hovering over logPokemon for log2 we can see the call signature matches the array we passed in logPokemon<string[]>(pokemon: string[]): string[] and log2 is the type string[].

Like weā€™ve seen in the first example we can be explicit when using generics and pass the type directly.

playground.ts
const log1 = logPokemon<string>('Pikachu')
const log2 = logPokemon<string[]>(['Bulbasaur', 'Charmander', 'Squirtle'])

You can also nest generics which can get hard to read, for example logPokemon<Array<Pokemon>>.

In the next example weā€™re going to look at a map array method implementation that takes advantage of generics.

Letā€™s think about the problem:

  • We canā€™t know the type of arr since youā€™re able to pass any type of array
  • The callback function could have any argument and return type based on that array
  • formatPokemon is of type any because we donā€™t know the return type
playground.ts
function map(
  arr: any,
  callback: (arg: any) => any
): any {
  return arr.map(callback)
}

const formatPokemon = map(
  ['Bulbasaur', 'Charmander', 'Squirtle'],
  (pokemon) => pokemon.toUpperCase()
)

// ["BULBASAUR", "CHARMANDER", "SQUIRTLE"]
console.log(formatPokemon)

So far we have only seen generics with one type variable but we can use as many type variables as we want.

Hovering over formatPokemon in the log we see itā€™s of type string[] and map has the signature type of map<string, string>(arr: string[], callback: (arg: string) => string): string[] as you would expect.

Using proper names for type variables makes the code so much easier to reason about.

playground.ts
function map<Input, Output>(
  arr: Input[],
  callback: (arg: Input) => Output
): Output[] {
  return arr.map(callback)
}

const formatPokemon = map(
  ['Bulbasaur', 'Charmander', 'Squirtle'],
  (pokemon) => pokemon.toUpperCase()
)

// ["BULBASAUR", "CHARMANDER", "SQUIRTLE"]
console.log(formatPokemon)

Generic Interface

An interface can also be generic.

Weā€™re creating a generic interface Dictionary with a index signature that accepts any string as the object key and itā€™s property has to match the shape of Type.

playground.ts
interface Dictionary<Type> {
  [key: string]: Type
}

interface Pokemon {
  name: string
  hp: number
  pokemonType: string
}

interface Consumable {
  name: string
  amount: number
}

const pokemon: Dictionary<Pokemon> = {
  1: { name: 'Bulbasaur', hp: 45, pokemonType: 'grass' },
  2: { name: 'Charmander', hp: 39, pokemonType: 'fire' },
  3: { name: 'Squirtle', hp: 44, pokemonType: 'water' }
}

const consumables: Dictionary<Consumable> = {
  1: { name: 'Antidote', amount: 4 },
  2: { name: 'Potion', amount: 8 },
  3: { name: 'Elixir', amount: 2 }
}

Generic Constraints

So far weā€™ve seen generics that work with any kind of value. We can use a constraint to limit what a type parameter can accept.

We use the extends keyword to say that Type has to have a length property.

playground.ts
interface Length {
  length: number
}

function itemLength<Type extends Length>(item: Type) {
  return item.length
}

// `array` has .length āœ…
const pokemonArrayLength = itemLength(
  ['Bulbasaur', 'Charmander', 'Squirtle']
)

// `string` has .length āœ…
const singlePokemonLength = itemLength('Pikachu')

// Argument of type 'number' is not assignable to
// parameter of type 'Length'. šŸš«
const numberLength = itemLength(1)

console.log(pokemonArrayLength) // 3
console.log(singlePokemonLength) // 7
console.log(numberLength) // undefined

The next example shows a sortPokemon function that uses a generic constraint to constrain the shape of the pokemon argument to the Pokemon interface.

playground.ts
interface Pokemon {
  name: string
  hp: number
}

type Stat = 'hp'

function sortPokemon<Type extends Pokemon>(
  pokemon: Type[],
  stat: Stat
): Type[] {
  return pokemon.sort(
    (firstEl, secondEl) => secondEl[stat] - firstEl[stat]
  )
}

const pokemon: Pokemon[] = [
  {
    name: 'Charmander',
    hp: 39
  },
  {
    name: 'Charmeleon',
    hp: 58
  },
  {
    name: 'Charizard',
    hp: 78
  },
]

// sort Pokemon by highest stat
console.log(sortPokemon(pokemon, 'hp'))

TypeScript generics are flexible. šŸ’Ŗ

Generic Constraints Using Type Parameters

In situations where we want to know if properties of an object we are trying to access exist we can use the keyof operator.

Before I show you that letā€™s first look at an example in JavaScript.

example.js
const pokemon = {
  hp: 35,
  name: 'Pikachu',
  pokemonType: 'electric'
}

In JavaScript we can use Object.keys to get the keys of an object.

example.js
Object.keys(pokemon) // ['hp', 'name', 'pokemonType']

We can do the type equivalent in TypeScript to make sure the property exists using the Key extends keyof Type syntax.

The keyof operator takes an object type and gives us a string or numeric literal union of its keys.

playground.ts
function getProperty<Type, Key extends keyof Type>(
  obj: Type,
  key: Key
) {
  return obj[key]
}

const pokemon = {
  hp: 35,
  name: 'Pikachu',
  pokemonType: 'electric'
}

// the property 'hp' exists on `pokemon` āœ…
getProperty(pokemon, 'hp')

// Argument of type '"oops"' is not assignable to
// parameter of type '"hp" | "name" | "pokemonType"'. šŸš«
getProperty(pokemon, 'oops')

Enums

Enums are a set of named constants.

You might be familiar with enums from other languages. Enums are a feature of TypeScript and donā€™t exist in JavaScript.

In JavaScript itā€™s popular to create an object with some constants to reduce the amount of typos.

example.js
const direction = {
  up: 'UP',
  right: 'RIGHT',
  down: 'DOWN',
  left: 'LEFT'
}
example.js
function movePokemon(direction) {
  console.log(direction) // 'UP'
}

movePokemon(direction.up)

This is very useful when you have some dynamic code.

example.js
function movePokemon(direction) {
  console.log(direction) // whatever got passed
}

movePokemon(direction[direction])

Enums are what you could use in such a case.

playground.ts
enum Direction {
  Up = 'UP',
  Right = 'RIGHT',
  Down = 'DOWN',
  Left = 'LEFT'
}

function movePokemon(direction: Direction) {
  console.log(direction) // UP
}

movePokemon(Direction.Up)

TypeScript compiles Enums to some interesting šŸ§ JavaScript code you can look at in the JavaScript output tab if youā€™re curious.

This looks like a fun šŸ˜… interview question.

output.js
var Direction

(function (Direction) {
    Direction["Up"] = "UP"
    Direction["Right"] = "RIGHT"
    Direction["Down"] = "DOWN"
    Direction["Left"] = "LEFT"
})(Direction || (Direction = {}))

You can learn more about enums since I just gave a brief overview.

Tuple

A tuple is an array with a fixed number of elements.

Use a tuple where the order is important.

playground.ts
type RGBColor = [number, number, number]

const color: RGBColor = [255, 255, 255]

You can specify optional values.

playground.ts
type RGBAColor = [number, number, number, number?]

const color: RGBAColor = [255, 255, 255, 0.4]

Cartesian coordinates anyone? šŸ—ŗļø

playground.ts
type CartesianCoordinates = [x, y]

const coordinates: CartesianCoordinates = [3, 4]

Classes

Using TypeScript doesnā€™t mean you have to use classes. That being said letā€™s explore what TypeScript adds to classes.

If youā€™re unfamiliar with classes you can read about Classes from the MDN Web Docs.

The readonly member prevents assignments to the field outside of the constructor.

playground.ts
class Pokemon {
  readonly name: string

  constructor(name: string) {
    this.name = name
  }
}

const pokemon = new Pokemon('Pikachu')

// Cannot assign to 'name' because it is a read-only property. šŸš«
pokemon.name = 'Charizard'

The public member can be accessed anywhere and itā€™s used by default, so you donā€™t have to type it unless you want to be explicit.

playground.ts
class Pokemon {
  public name: string

  constructor(name: string) {
    this.name = name
  }
}

const pokemon = new Pokemon('Pikachu')

pokemon.name = 'Charizard' // no problem āœ…

The protected member is only visible to subclasses of the class itā€™s declared in.

playground.ts
class Pokemon {
  protected name: string

  constructor(name: string) {
    this.name = name
  }
}

class LogPokemon extends Pokemon {
  public logPokemon() {
    console.log(this.name)
  }
}

const pokemon = new LogPokemon('Pikachu')

// Property 'name' is protected and only accessible within
// class 'Pokemon' and its subclasses. šŸš«
pokemon.name

// no problem āœ…
pokemon.logPokemon() // 'Pikachu'

The private member is like protected but doesnā€™t allow access to the member even from subclasses.

playground.ts
class Pokemon {
  private name: string

  constructor(name: string) {
    this.name = name
  }
}

class LogPokemon extends Pokemon {
  public logPokemon() {
    // Property 'name' is private and only
    // accessible within class 'Pokemon'. šŸš«
    console.log(this.name)
  }
}

const pokemon = new LogPokemon('Pikachu')

// Property 'name' is protected and only accessible within
// class 'Pokemon' and its subclasses. šŸš«
pokemon.name

// no problem āœ…
pokemon.logPokemon() // 'Pikachu'

TypeScript gives us a shorter syntax for declaring a class property from the constructor using parameter properties by prefixing it with public, private, protected, or readonly.

playground.ts
class Pokemon {
  name: string

  constructor(name: string) {
    this.name = name
  }
}

class Pokemon {
  constructor(public name: string) {
    // no body necessary
  }
}

You can implement šŸ¤­ an interface using the implements keyword.

playground.ts
interface LogPokemon {
  logPokemon(): void
}

class Pokemon implements LogPokemon {
  constructor(public name: string) {}

  public logPokemon() {
    console.log(this.name)
  }
}

class Pokedex implements LogPokemon {
  // Property 'logPokemon' is missing in type 'Pokedex' but
  // required in type 'LogPokemon'. šŸš«
}

The syntax for a generic class is similar to a generic interface syntax.

playground.ts
class Pokemon<Type> {
  constructor(public pokemon: Type) {}
}

// Pokemon<string> āœ…
const pokemonString = new Pokemon('Pikachu')

// Pokemon<string[]> āœ…
const pokemonArray = new Pokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

TypeScript introduces the concept of an abstract class thatā€™s a contract used by other classes to extend from. The abstract class doesnā€™t contain implementation and canā€™t be instantiated.

playground.ts
abstract class Pokemon {
  constructor(public name: string) {}

  abstract useItem(item: string): void
}

class FirePokemon extends Pokemon {
  useItem(item: string) {
    console.log(`${this.name} used šŸ§Ŗ ${item}`)
  }
}

const charizard = new FirePokemon('Charizard')

charizard.useItem('Potion') // 'Charizard used šŸ§Ŗ Potion.'

We glanced over classes in TypeScript because itā€™s a large topic deserving itā€™s own post.

If classes are something you want to learn more about using in TypeScript read the Classes section in the TypeScript Handbook.

In the next section weā€™re going to briefly introduce another concept that TypeScript introduced to classes ā€” decorators.

Decorators

Decorators are like higher-order functions (function that takes another function as an argument or returns a function) that can hook into a class and itā€™s methods and properties, so we end up with composable and reusable pieces of code logic.

JavaScript has a proposal for decorators, so they might get added to the language in the future.

I want to preface this by saying that decorators are primarily used by library authors and frameworks such as Angular and NestJS.

Donā€™t sweat it when you would use decorators because theyā€™re mainly given to you to use them to improve your developer experience without having to think about how it works.

If youā€™re using decorators you have to enable experimental support for decorators inside tsconfig.json.

tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

In the next example weā€™re going to see how we can use a method decorator for validation to check if a Pokemon has enough experience to evolve by using @requiredExperience above the evolve method.

The decorator implementation is inside requiredExperience thatā€™s also known as a decorator factory because it returns a function that will be called by the decorator at runtime.

playground.ts
function requiredExperience() {
  return function(
    target: any,
    propertyKey: string,
    descriptor: any
  ) {
    // the original method
    const originalMethod = descriptor.value

    // overwrite the method
    descriptor.value = function(...args: any[]) {
      // if check passes...
      if (this.experience > this.experienceThreshold) {
        // use original method
        originalMethod.apply(this, args)
      } else {
        // otherwise do something else
        console.log(`${this.name} doesn't have enough experience to evolve into ${this.evolution}. šŸš«`)
      }
    }

    return descriptor
  }
}

class Pokemon {
  constructor(
    private name: string,
    private experience: number,
    private evolution: string,
    private experienceThreshold: number
  ) {}

  @requiredExperience()
  evolve() {
    console.log(`${this.name} evolved to ${this.evolution}. āœØ`)
  }
}

const pikachu = new Pokemon('Pikachu', 80, 'Raichu', 120)

// "Pikachu doesn't have enough experience to
// evolve into Raichu." šŸš«
pikachu.evolve()

We barely touched upon decorators because it would get lengthy and itā€™s enough you just know about them.

Hooking into code like this is called metaprogramming and is often more useful in frameworks where you might need to add metadata to measure or analyze code.

Thereā€™s a great video by Fireship on The Magic of TypeScript Decorators you should watch. šŸæ

If you want to dive deep into decorators you can read Decorators from the TypeScript Handbook.

Set Up TypeScript

Youā€™re probably learning TypeScript to use with a JavaScript framework and thatā€™s great because most popular JavaScript frameworks require zero configuration for enabling TypeScript support. You should consult the docs for your framework how to set up TypeScript.

If you want to use vanilla TypeScript on a passion project I highly recommend using Vite. To set up your project just run npm init vite and pick a template.

Letā€™s look at how to set up TypeScript from scratch and show you around the TypeScript compiler settings.

Start by initializing a project inside an empty folder.

terminal
npm init -y

Install TypeScript as a development dependency.

terminal
npm i -D typescript

Next create a app.ts file at the root of the project and add some TypeScript code.

app.ts
const pokemon: string = 'Pikachu'

TypeScript setup

Since your web browser and Node donā€™t understand TypeScript we have to transpile the TypeScript code to JavaScript.

terminal
npx tsc app.ts

TypeScript compiler

npm includes a npx tool that runs executables without having to install a package globally. It just downloads the binary to your .bin folder in node_modules and removes it when youā€™re done.

The tsc command invokes the TypeScript compiler that creates a app.js file. This is what your browser and Node would run.

It would be a drag having to run this each time when you make a change, so you can pass a watch flag to the TypeScript compiler.

terminal
npx tsc app.ts -w

Itā€™s easier to add a command to scripts in your package.json.

package.json
{
  "name": "typescript",
  "scripts": {
    "dev": "npx tsc app.ts -w"
  },
  "devDependencies": {
    "typescript": "^4.3.5"
  }
}

This lets us just run npm run dev.

terminal
npm run dev

TypeScript watch

If youā€™re not using Vite and want live reload for your site you can use live-server together with TypeScript at the same time.

package.json
{
  "name": "example",
  "scripts": {
    "dev": "live-server && npx tsc pokemon.ts -w"
  },
  "devDependencies": {
    "typescript": "^4.3.5"
  }
}

You only have to include the JavaScript file in your HTML file. Using the defer attribute loads the JavaScript code after the DOM (Document Object Model) has loaded.

example.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>TypeScript</title>
    <script src="app.js" defer></script>
  </head>
  <body>
    <h1>TypeScript</h1>
  </body>
</html>

If youā€™re a Node chad šŸ’Ŗ you can use ts-node that lets you run TypeScript files without having to transpile them first by using ts-node app.ts.

You also get a live code environment (REPL) if you run ts-node like you would typing node in your terminal.

ts-node

If youā€™re using ts-node you can use nodemon to watch the files by creating a nodemon app.ts script in package.json.

Tooling is a subject that deserves itā€™s own post. I just want to expose you to what is out there.

Letā€™s look at the TypeScript compiler options.

TypeScript looks for a tsconfig.json file that we can create by hand or generate by using the --init flag.

terminal
npx tsc --init

TSConfig

Inside the generated tsconfig.json file we can see useful descriptions alongside the TypeScript compiler options.

You can look at the TypeScript compiler options and the TSConfig Reference that explains each option if you want to learn more.

Letā€™s make the tsconfig.json file more readable by removing most of the TypeScript compiler options and focusing on a couple of options.

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["ESNext", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "noEmitOnError": true,
    "strict": true
  }
}
  • target: specifies the target JavaScript version the TypeScript code compiles to where ESNext is the latest
  • module: specifies the module system weā€™re going to transpile to where CommonJS uses the const package = require('package') syntax and the ES6 and onwards option uses ECMAScript modules (ESM) import package from 'package' syntax which is supported natively in browsers only as of recent
  • lib: decides what types to include with our code because we might not need DOM types if weā€™re writing for Node or want to target some other JavaScript features (TypeScript includes a default set of type definitions for built-in JavaScript APIs)
  • outDir: specifies the output directory for transpiled JavaScript code
  • rootDir: specifies the root directory for TypeScript files
  • noEmitOnError: doesnā€™t output anything if thereā€™s a TypeScript error
  • strict: enables all the strict flags so our code is more type-safe

Since we set the rootDir we donā€™t have specify what file to run inside package.json since it watches the entire directory.

package.json
"scripts": {
  "dev": "npx tsc -w"
}

TypeScript output

If you want to fire up a quick TypeScript demo you can use an online editor like CodeSandbox so you can try things out for any project or framework.

Hope this helps you get started using TypeScript.

Reading Type Definitions

TypeScript gives us documentation inside our editor.

This is useful as code completion since you can dive into the types directly to understand the surface area of an API.

If you just started to learn TypeScript donā€™t be intimidated as it requires practice to understand the types first.

We can inspect the generic Array interface by selecting Array and pressing F12 or right-clicking it and selecting Go to Definition.

example.ts
const pokemon: Array<string> = ['Bulbasaur', 'Charmander', 'Squirtle']

Array interface

Thereā€™s always going to be a lot of information in type definition files, so focus on what youā€™re inspecting. You can jump from one type definition to another by doing the same to other types inside.

Thereā€™s a couple of interesting things to note:

  • The file ends with a .d.ts extension which is just a declaration file for types (itā€™s used to add TypeScript types to things that arenā€™t built with TypeScript)
  • Thereā€™s multiple type declarations on the right depending on the version of JavaScript the features were added in among other things (remember itā€™s one interface since we can declare it again)

If we poke around lib.es2015.core.d.ts we can find the types that describe how to create arrays on the fly.

Array constructor

If we go back and look at lib.es5.d.ts on the right it looks like something we would expect. You can double-click it or Alt + Click the type definition to open it in a separate tab.

es5 interface

This has properties we would expect like length, pop, push, and map.

By looking at the type definitions we can learn how they typed the map array method.

example.ts
interface Array<T> {
  // ...
  map<U>(
    callbackfn: (
      value: T,
      index: number,
      array: T[]
    ) => U, thisArg?: any
  ): U[]
  // ...
}

You can get a real idea how generics are used in practice.

Reading TypeScript Errors

Arguably, the hardest part about TypeScript can be reading and understanding errors.

First open the problems tab in your editor by pressing Ctrl + Shift + M so you have an easier time reading TypeScript errors.

example.ts
const pokemon = {
  bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
  charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
  squirtle: { id: 3, hp: 44, attack: 48, defense: 65 },
}

const bulbasaur: string = 'bulbasaur'

// TypeScript is mad. šŸ™…ā€ā™€ļø
const chosenPokemon = pokemon[bulbasaur]

Problems tab

error
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ bulbasaur: { id: number; hp: number; attack: number; defense: number; }; charmander: { id: number; hp: number; attack: number; defense: number; }; squirtle: { id: number; hp: number; attack: number; defense: number; }; }'.
  No index signature with a parameter of type 'string' was found on type '{ bulbasaur: { id: number; hp: number; attack: number; defense: number; }; charmander: { id: number; hp: number; attack: number; defense: number; }; squirtle: { id: number; hp: number; attack: number; defense: number; }; }'.

Sometimes youā€™re going to have these sort of errors that make you question your sanity when using TypeScript when itā€™s a simple fix.

The error message is verbose because itā€™s describing the entire object as a literal type since weā€™re not using a type alias or interface it can refer to instead.

error
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Pokemon'.
  No index signature with a parameter of type 'string' was found on type 'Pokemon'.

You can read TypeScript errors like sentences with because inbetween.

error
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type ...
  because no index signature with a parameter of type 'string' was found on type ...

The further you go down the because chain the more specific the error is, so start from there.

This problem might be harder to reason about if you donā€™t understand things like object index signatures tying everything we learned so far together.

The second part of the error message reveals the problem.

We donā€™t care if chosenPokemon type is any because thatā€™s what TypeScript cares about ā€” we care about what causes it.

This is what TypeScript thinks is going on.

example.ts
// TypeScript sees the general type `string` šŸš«
const chosenPokemon = pokemon['random string']

// TypeScript expects the literal type `bulbasaur` āœ…
const chosenPokemon = pokemon['bulbasaur']

TypeScript freaks out because it thinks weā€™re passing some random string since we havenā€™t specified the index signature.

If you remember, objects are number indexed by default.

example.ts
interface Pokemon {
  [index: string]: {
    id: number
    hp: number
    attack: number
    defense: number
  }
}

const pokemon: Pokemon = {
  bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
  charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
  squirtle: { id: 3, hp: 44, attack: 48, defense: 65 },
}

const bulbasaur: string = 'bulbasaur'

const chosenPokemon = pokemon[bulbasaur] // ok āœ…

That works, but itā€™s not what we want.

We just want TypeScript to infer the type for us here so we donā€™t have to do it by hand, otherwise we would have to update the interface each time thereā€™s another Pokemon.

example.ts
interface Pokemon {
  bulbasaur: {
    id: number
    hp: number
    attack: number
    defense: number
  }
  charmander: {
    id: number
    hp: number
    attack: number
    defense: number
  }
  squirtle: {
    id: number
    hp: number
    attack: number
    defense: number
  }
}

The problem is that weā€™re not passing the type literal bulbasaur, so TypeScript canā€™t compare it to the key bulbasaur in pokemon.

example.ts
const pokemon = {
  bulbasaur: { id: 1, hp: 45, attack: 49, defense: 49 },
  charmander: { id: 2, hp: 39, attack: 52, defense: 43 },
  squirtle: { id: 3, hp: 44, attack: 48, defense: 65 },
}

const bulbasaur: 'bulbasaur' = 'bulbasaur'

const chosenPokemon = pokemon[bulbasaur] // āœ…

Youā€™re going to encounter this when dealing with dynamic values.

If you used const it would infer it as a literal type, so the type assignment isnā€™t required and itā€™s only there so itā€™s obvious.

Thereā€™s times when TypeScript goes cuckoo for Cocoa Puffs and something goes wrong. Instead of closing and opening your editor just restart the TypeScript server by pressing F1 to open the command palette in VS Code and find TypeScript: Restart TS server.

Restart TS server

If you donā€™t understand the error open up the TypeScript Playground and reproduce it there so you have a shareable link you can send to anyone.

You can always ask a question in the TypeScript Community Discord Server.

Dealing With Untyped Libraries

Almost every library you want to use supports TypeScript today.

The great thing about TypeScript is that the community can gather around to create types for libraries that donā€™t use TypeScript.

Ambient declarations describe the types that would have been there if the project was written in TypeScript and they have the .d.ts file extension that TypeScript picks up.

DefinitelyTyped is a project that holds a repository for types that everyone gathered around to contribute types having over 80,000 commits and itā€™s used to search for types on the official TypeScript page.

Type search

In the example we have a simple HTTP server in Node that sends a JSON response.

app.ts
const http = require('http')

function requestListener(req, res) {
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ pokemon: 'Pikachu' }))
}

const server = http.createServer(requestListener)
server.listen(8080)

Type definitions

You donā€™t have to use search to figure out if what youā€™re using is typed or not ā€” TypeScript is going to let you know.

To install a type definition you just have to do npm i -D @types/package to install it as a development dependency.

Letā€™s install the types for Node.

terminal
npm i -D @types/node

Require to import

TypeScript lets us know we can convert require to an import.

app.ts
import * as http from 'http'

You have to keep in mind these older libraries use CommonJS that uses module exports, so we canā€™t expect it to use ECMAScript modules where you can import a package simply as import http from 'http' using the default export syntax or import { http } from http syntax if itā€™s a named import.

Try it first and if it doesnā€™t work you can use import * as http from 'http' to import the entire moduleā€™s contents.

You can brush up on how JavaScript modules work by reading JavaScript modules on the MDN Web Docs.

We still havenā€™t resolved the types for req and res for requestListener since we canā€™t do that alone by just installing type definitions.

How do we figure the types in this case?

You can always search for the answer and thatā€™s a valid approach but letā€™s dig through the type definitions to figure it out.

Letā€™s select the http part and press F12 to Go to Definition. This is going to open the http.d.ts type declaration file.

Inside the file we can do a Ctrl + F search for requestListener. If that wouldnā€™t work we coud look for req or res until we find something.

Type declaration search

We found our types! šŸŽ‰ The req argument expects IncomingMessage and the res argument expects ServerResponse.

We can import the types from the http package.

app.ts
import * as http from 'http'

import type { IncomingMessage, ServerResponse } from 'http'

The type keyword is optional but it makes it clear weā€™re using types and not importing some method.

app.ts
import * as http from 'http'

import type { IncomingMessage, ServerResponse } from 'http'

function requestListener(req: IncomingMessage, res: ServerResponse) {
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ pokemon: 'Pikachu' }))
}

const server = http.createServer(requestListener)
server.listen(8080)

Now we have the entire http API at our fingertips we can look at without leaving our editor.

Using types is easy in most cases when itā€™s properly documented in a project that uses TypeScript. I wanted to show you how to deal with a scenario where that isnā€™t the case.

In the rare case when thereā€™s no types for a package you can create a index.d.ts file and place it inside a types folder. The name could be anything.

index.d.ts
declare module 'http'

This says to TypeScript the package exists and itā€™s going to stop bothering you.

Generate Types

In case where we have some complex JSON object from an API response we can generate types from it to make our lives easier instead of typing it out by hand.

We can use quicktype to generate types for more than just TypeScript. Thereā€™s also a quicktype extension for VS Code.

Their default example uses Pokemon, so itā€™s perfect! Consistency. šŸ’Ŗ

Convert JSON into gorgeous, typesafe code in any language

This is also great as a learning tool.

Conclusion

You learned a lot about TypeScript fundamentals to give you confidence in using it in your projects. TypeScript itself is a tool that gives confidence about your code but donā€™t forget it doesnā€™t save you at runtime.

TypeScript is only gaining more popularity and since more projects are using TypeScript that means we as developers have to step up if we want to contribute to those projects.

Even if TypeScript fades and JavaScript gets types we didnā€™t have to learn another language to use types because TypeScript is JavaScript.

Thank you for your time! šŸ˜„

Support

You can subscribe on YouTube, or consider becoming a patron if you want to support my work.

Patreon
Found a mistake?

Every post is a Markdown file so contributing is simple as following the link below and pressing the pencil icon inside GitHub to edit it.

Edit on GitHub