Intermediate 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 tostring
.
- Extract from
- 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 anextends
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
ornumber
, and not a subset of strings or numbers.
- Use of the
- 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 usstring | 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 (-
) beforereadonly
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 withquery
type QueryKeys = Extract<keyof Document, `query${string}`>