— typescript, javascript — 2 min read
Typescript has been my go-to language every time I start a new project. Typescript helps us to describe objects and to make inline documentation better. One of the things I love about typescript is it already provide utility function to facilitate common type transformations. This time we are going to talk about some of these utility types that I often use everyday.
Partial<T>
is the utility type that I use the most. This type makes all the properties of an interface optional.
1type Cars = {2 brand: string3 color: string4}5
6const helloCars = ( cars: Partial<Cars> ) => {7 if(cars.color) {8 console.log(`That is a ${cars.color} ${cars.brand}`)9 } else {10 console.log(`That is a ${cars.brand}`)11 }12}13
14helloCars({ brand: "Honda", color: "yellow"}) // That is a yellow Honda15helloCars({ brand: "Mercedes" }) // That is a Mercedes
In the example above, we can see that we can call the helloCars
function without defining the "colors" property because when we use Partial<Cars>
, the object will change its annotation as you can see below.
1// Partial<Cars> 2{3 brand?: string,4 color? string5}
In contrast to Partial<T>
, Required<T>
actually makes all fields of T
required
1// Required<Cars> 2{3 brand: string,4 color string5}
As the name suggests, Readonly<T>
makes all fields in the interface read-only. This means that they become immutable and cannot be reassigned.
1interface Cars {2 brand: string3 color: string4}5
6const myCars: Readonly<Cars> = {7 brand: "BMW",8 color: "white"9}10
11// Cannot assign to 'brand' because it is a read-only property.12myCars.brand = "Datsun"13// Cannot assign to 'color' because it is a read-only property.14myCars.color = "yellow"
With Record<K,T>
we can construct an object type using a union K
as our keys and T
as the value. Let's use the fields from the previous example.
1type keys = "brand" | "color"2
3type Cars = Record<keys, string>4
5const helloCars = ( cars: Cars ) => {6 console.log(`You have a nice ${cars.color} ${cars.brand}`)7}8
9helloCars({ brand: "BMW", color: "black" }) // You have a nice black BMW
With that union keys
, you can add as many keys as you like. This will become a problem when you have a big object and wants to restrict the keys depends on an object. Luckily, we can solve this problem by using the keyof
1interface Cars = {2 brand: string3 color: string4}5
6const restrictedCars: Record<keyof Cars, string> = {7 brand: "Mercedes",8 color: "red",9 unexpectedKey: "value" // will not allowed since we only allowed 'brand' and 'color'10}
There will be a time when you just want to pick some keys from an interface and construct a new one. This is when Pick<I, K>
comes into play. Pick<I, K>
is a utility type that constructs an interface by picking K
fields that are listed in I
.
1interface Cars = {2 brand: string3 color: string4 year: number5}6
7interface PickCars = Pick<Cars, "color" | "year">8
9const restrictedCars: PickCars = {10 brand: "Mercedes", // will error since we just pick 'color' and 'year' from Cars11 color: "red",12 year: 202013}
And there is Omit<I, K>
that does the exact opposite of Pick<I,K
, Omit<I,K>
constructs an interface by picking all fields in I
and then remove K
keys.
1interface Cars = {2 brand: string3 color: string4 year: number5}6
7interface OmitCars = Omit<Cars, "brand">8
9const restrictedCars: OmitCars = {10 brand: "Mercedes", // will error since we omit 'brand' from Cars11 color: "red",12 year: 202013}
The 2 examples above will return the same interface since they contain "color" and "year" in our constructed type.
What if we want to Pick and Omit but on union type? Luckily, typescript provides a similar behavior with Extract and Exclude.
Extract<T, U>
constructs a type by picking types from T
that are a subset of U
. However, Exclude<T, U>
do the opposite by excluding types from T
that are a subset of U
. Let's see a few examples.
1type AllDrinks = "teh" | "kopi" | "susu" | "jus" | 99 | (() => string)2type DrinksWeLike = "teh" | "susu" | "bandrek"3
4type DrinksExtracted = Extract<AllDrinks, string> // "teh" | "kopi" | "susu" | "jus"5type AvailableDrinks = Extract<DrinksExtracted, DrinkWeLike> // "teh" | "susu"6
7type DrinksExcluded = Exclude<AllDrinks, number | Function > // "teh" | "kopi" | "susu" | "jus"8type DrinksWeHate = Exclude<DrinksExcluded, DrinkWeLike> // "kopi" | "jus"
NonNullable<T>
is a utility type that we can use to avoid null
or undefined
.
1type Colors = "green" | "red" | undefined | null2
3type CorrectColors = NonNullable<Colors> // "green" | "red"
But the interesting part is, this type is only worked if you use —strictNullChecks
flag or set strictNullChecks: true
on your tsconfig.
For example, this code is using strictNullChecks: false
, as you can see the variable can still be assigned to undefined
and this is using strictNullChecks: true
, the TS compiler will tell you immediately about the error.
ReturnType<F>
returns the return type definition of F
. To avoid an error, remember to always fill the F
with function. Here is some example.
1type stringF = ReturnType<() => string> // string2type numberF = ReturnType<() => number> // number3type unknownF = ReturnType<<T>() => T> // unknown4type wrongType1 = ReturnType<number> // Error: Type 'number' does not satisfy the constraint '(...args: any) => any'.5type wrongType2 = ReturnType<Function> // Error: Type 'Function' does not satisfy the constraint '(...args: any) => any'.
Moreover, you also can get a return type of an existing function by utilizing typeof
1function helloCars() {2 return {3 brand: "Honda",4 color: "yellow"5 }6}7
8type F = ReturnType<typeof helloCars>9// type F = {10// brand: string;11// color: string;12// }
There are many other utility types you can use to help you construct your type but I hope these 7 utilities will help you to start understanding how to use them on your code. Please reach me on twitter on linkedIn if you have any questions or feedback. Hope we'll meet on another article, until then, stay great!