Avatar

Jambo, I'm Julia.

Intermediate Typescript

#typescript

7 min read

I’ve been delving deeper into the Typescript language to learn more advanced types. Here are some notes from a recent course I did.

Type Queries

keyof

  • Get the type representing all of the property keys on an interface.
  • From here, you can filter the return type further by using the intersection operator (&).
type StringNames = keyof String

type StringPropertyNames = StringNames & string
// "toString" | "charAt" | "charCodeAt" | ...
type SymbolPropertyNames = StringNames & symbol
// typeof Symbol.iterator

typeof

  • Get the type of a value.
  • A good use case for this could be the response type from an API call.
async function doSomething() {
  const response = await Promise.all([fetch('https://test.com'), Promise.resolve(1), Promise.resolve('Hurrah')])

  type ResponseType = typeof response
  // [Response, number, string]
}

Conditional Types

  • We can conditionally set a type using the ternary operator syntax, generics and the extends keyword.
  • extends is currently the only mechanism in Typescript that allows us to express a condition.
class Cat {
  startChasingMouse() {}
  stopChasingMouse() {}
}

class Fish {
  startSwimming() {}
}

type Pet<T> = T extends 'cat' ? Cat : Fish

let pet_cat: Pet<'cat'>
// pet_cat: Cat
let pet_fish: Pet<'notacat'>
// pet_fish: Fish
  • The way I read it is:
    • T extends "cat" is the condition i.e. in this case, does T have the value “cat”.
    • If it does, return the class Cat (which in Typescript also means the type Cat).
    • Otherwise, return Fish.

Utility Types

Extract

  • Used to define a new type that’s a sub-section of an existing type.
type Animals = 'lion' | 'tiger' | 'cat' | [string, string, string] | { puppy: string; dog: string }

type StringAnimals = Extract<Animals, string>
// "lion" | "tiger" | "cat"
type ObjectAnimals = Extract<Animals, { dog: string }>
// { puppy: string; dog: string; }
type TupleAnimals = Extract<Animals, [string, string, string]>
// [string, string, string]
  • The way I read Extract<Animals, string> is:
    • Extract from Animals , a subset that is assignable to string.
  • The way it works:
type Extract<T, U> = T extends U ? T : never

Exclude

  • This is the opposite of Extract in that it filters out a sub-section of an existing type.
type Animals = 'lion' | 'tiger' | 'cat' | [string, string, string] | { puppy: string; dog: string }

type NonStringAnimals = Exclude<Animals, string>
// [string, string, string] | { puppy: string; dog: string; }
  • The way it works:
type Exclude<T, U> = T extends U ? never : T

Inference with conditional types

  • The infer keyword lets us extract and obtain type information from a larger type. It can only be used in an extends clause.
  • For example, if we wanted to infer the type for a function’s second argument:
type FunctionSecondArg<T> = T extends (first: any, second: infer SecondArgument, ...args: any[]) => any
  ? SecondArgument
  : never

type NewType = FunctionSecondArg<(name: string, numberOfSpecies: number) => void>
// number
  • Another example is to infer a promise return type:
type PromiseReturnType<T> = T extends Promise<infer Return> ? Return : T

type NewType = PromiseReturnType<Promise<string>>
// string

Indexed Access Types

  • Allows us to get parts of an array or object type with indices.
interface Animal {
  species: string
  habitat: string
  numberOfSubSpecies: number
  continentsFoundIn: {
    africa: string
    asia: string
    europe: string
  }
}

let continents: Animal['continentsFoundIn']
// { africa: string; asia: string; europe: string; }

let continent: Animal['continentsFoundIn']['africa']
// string
  • We can also use the union operator:
let continentsOrNumberSubSpecies: Animal['numberOfSubSpecies' | 'continentsFoundIn']
// number | { africa: string; asia: string; europe: string; }

Mapped Types

  • More flexible than using the index to define new types.

Dictionary type

type Pet = {
  name: string
  species: string
  friends: number
}

type MyDict<T> = { [k: string]: T } // <- index signature

const petStore: MyDict<Pet> = {}
petStore.cat // <- Has type Pet

Mapped type

  • The example above works, but allows any key to be added to the petStore object. We can be more specific about the keys allowed with a mapped type.
type MyRecord = { [PetKey in 'cat' | 'dog']: Pet } // <- mapped type

function getPetStore(petStore: MyRecord) {
  petStore.cat // <- Has type Pet
  petStore.dog // // <- Has type Pet

  petStore.house // <- Error: Property 'house' does not exist on type 'MyRecord'
}
  • Differences between mapped type and index signatures:
    • Use of the in keyword, rather than :
    • Index signatures must be all on string or number, and not a subset of strings or numbers.
  • To make our example generic, we’d do this:
type MyRecord<KeyType extends string, ValueType> = {
  [Key in KeyType]: ValueType
}

// Which is a built-in Typescript type
type Record<K extends keyof any, T> = {
  [P in K]: T
}
  • Note that keyof any just gives us string | number | symbol.

Combining mapped types and indexed access types

  • We can use the index to define the value types e.g.
type PickDateProperties<Keys extends keyof Date> = {
  [Key in Keys]: Date[Key]
}

type DateSubset = PickDateProperties<'getDay' | 'getHours' | 'getTime'>
// { getTime: () => number; getDay: () => number; getHours: () => number; }
  • To make our example generic, we’d do this:
type PickProperties<ValueType, Keys extends keyof ValueType> = {
  [Key in Keys]: ValueType[Key]
}

type DateSubset = PickProperties<Date, 'getDay' | 'getHours' | 'getTime'>

// Which is a built-in Typescript type
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

Applying modifiers

  • We can modify each value type to make them readonly and / or optional (?). Note that (-) before readonly or ? indicates the removal of the modifier.
  • These are already in-built into Typescript.
/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P]
}

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P]
}

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

Template literal types

  • Uses the same syntax as the ECMAScript template literal, within a type expression.
type Pets = 'cat' | 'dog' | 'snake'

type Colours = 'brown' | 'black' | 'green' | 'yellow'

type PetColours = `my_${Colours}_${Pets}`
// "my_brown_cat" | "my_brown_dog" | "my_brown_snake" | "my_black_cat" | ...
  • Typescript provides some helper types for use with template literal types.
    • UpperCase
    • LowerCase
    • Capitalize
    • Uncapitalize
type CamelCasePetColours = `my${Capitalize<Colours>}${Capitalize<Pets>}`
// "myBrownCat" | "myBrownDog" | ...
  • This can come in handy when you want to perform key mapping, using the as keyword to redefine the type.
type NewType = {
  [K in keyof I as `set${Capitalize<K>}`]: I[K]
}
  • Another use case would be to combine with Extract to filter for specific key names e.g. in this case, to return only the methods starting with query
type QueryKeys = Extract<keyof Document, `query${string}`>

© 2016-2024 Julia Tan · Powered by Next JS.