27
Getting Started With Built-in Utility Types in TypeScript Part 2
TypeScript comes with a number of built-in utility types. These types can be very useful. They can help you create new, and also very advanced, types from existing with only a bit of code. In this article, you will learn about some of these utility types, how they work and also how to use them.
The goal of this two-part article is to help you understand TypeScript built-in utility types. In this second part, you will learn about the last eight of these types. These are NonNullable
, Parameters
, ConstructorParameters
, ReturnType
, InstanceType
, ThisParameterType
, OmitThisParameter
and ThisType
.
If you want to learn about the previous eight, take a look at the part 1. Part 1 will help you understand and work with Partial
, Required
, Readonly
, Record
, Pick
, Omit
, Exclude
and Extract
. Otherwise, keep reading.
The types we will talk about in this part are available in TypeScript version 4.0 and newer. Make sure you use this version to avoid potential issues due to compatibility.
The utility types in TypeScript we will talk about today all use very similar syntax. It is almost identical. The only difference being the name of each utility type. As we discussed in previous part, utility types can be confusing at first. What can help you understand them is trying to see these types as functions.
This is what these types basically are. They are functions that take some input and return some output. In this case, input and output are types. The rest is just a plain function. The types you will learn about in this part all require a single parameter, some type. You pass this parameter between angle brackets.
These angle brackets, the left- and right-pointing (<>
), follow right after the name of the utility type. The name of the type always starts with capital letter. This is very similar to calling a normal JavaScript function. You only swapped the parentheses for brackets and started the function name with a capital letter.
Sometimes, TypeScript utility types require more than one parameter. In that case, you pass these parameters between the angle brackets a usually and separate them with a colon (,
). That's about the syntax. Now, let's talk about the actual types.
The NonNullable
utility type works similarly to the exclude. It takes some type you specified and returns that type excluding all null
and undefined
types.
// Create a type:
type prop = string | number | string[] | number[] | null | undefined
// Create new type based on previous type
// excluding null and undefined:
type validProp = NonNullable<prop>
// Translates to:
// type validProp = string | number | string[] | number[]
// This is valid:
let use1: validProp = 'Jack'
let use2: validProp = null
// TS error: Type 'null' is not assignable to type 'validProp'.
The Parameters
type returns a tuple type that contains the types of the parameters function you passed as Type
. These parameters are returned in the same order in which they appear in the function. Note that the Type
argument, in case of this and following types, is a function ((...args) => type
), not a type such as string
.
// Declare a function type:
declare function myFunc(num1: number, num2: number): number
// Use Parameter type to create new tuple type
// from parameters of myFunc function:
type myFuncParams = Parameters<typeof myFunc>
// Translates to:
// type myFuncParams = [num1: number, num2: number]
// This is valid.
let someNumbers: myFuncParams = [13, 15]
let someMix: myFuncParams = [9, 'Hello']
// TS error: Type 'string' is not assignable to type 'number'.
// Use Parameter type to create new tuple type
// from function type:
type StringOnlyParams = Parameters<(foo: string, fizz: string) => void>
// Translates to:
// type StringOnlyParams = [foo: string, fizz: string]
// This is valid.
let validNamesParams: StringOnlyParams = ['Jill', 'Sandy']
let invalidNamesParams: StringOnlyParams = [false, true]
// TS error: Type 'boolean' is not assignable to type 'string'.
The ConstructorParameters
type is very similar to the Parameters
type. The difference between these two is that while Parameters
gets types from function arguments, the ConstructorParameters
gets types from the constructor you pass as the Type
argument.
// Create a class:
class Human {
public name
public age
public gender
constructor(name: string, age: number, gender: string) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
// Create type based on Human constructor:
type HumanTypes = ConstructorParameters<typeof Human>
// Translates to:
// type HumanTypes = [name: string, age: number, gender: string]
const joe: HumanType = ['Joe', 33, 'male']
const sandra: HumanType = ['Sandra', 41, 'female']
const thomas: HumanType = ['Thomas', 51]
// TS error: Type '[string, number]' is not assignable to type '[name: string, age: number, gender: string]'.
// Source has 2 element(s) but target requires 3.
// Create type based on Human constructor:
type StringType = ConstructorParameters<StringConstructor>
// Translates to:
// type StringType = [value?: any]
The ReturnType
is also similar to the Parameters
type. Difference here is that the ReturnType
extracts the return type of a function you pass as the Type
argument.
// Declare type for function:
declare function myFunc(name: string): string
// Use ReturnType to extract return type
// from myFunc type.
type MyFuncReturnType = ReturnType<typeof myFunc>
// Translates to:
// type MyFuncReturnType = string
// This is valid:
let name1: MyFuncReturnType = 'Victoria'
// This is valid:
let name2: MyFuncReturnType = 42
// TS error: Type 'number' is not assignable to type 'string'.
type MyReturnTypeBoolean = ReturnType<() => boolean>
// Translates to:
// type MyReturnTypeBoolean = boolean
type MyReturnTypeStringArr = ReturnType<(num: number) => string[]>
// Translates to:
// type MyReturnTypeStringArr = string[]
type MyReturnTypeVoid = ReturnType<(num: number, word: string) => void>
// Translates to:
// type MyReturnTypeVoid = void
The InstanceType
is a bit complicated. What it does is it creates a new type from the instance type of a constructor function you passed as argument for Type
. You will probably don't need this utility type if you work class with a regular class. You can just use the class name to get the instance type you want.
class Dog {
name = 'Sam'
age = 1
}
type DogInstanceType = InstanceType<typeof Dog>
// Translates to:
// type DogInstanceType = Dog
// Similar to using the class declaration:
type DogType = Dog
// Translates to:
// type DogType = Dog
The ThisParameterType
extracts the type of use for the this
parameter for the function you passed as the Type
argument. If the function doesn't have this
parameter, the utility type will return unknown
.
// Create a function with this parameter:
function capitalizeString(this: String) {
return this[0].toUpperCase + this.substring(1).toLowerCase()
}
// Create type based on capitalizeString function this parameter:
type CapitalizeStringType = ThisParameterType<typeof capitalizeString>
// Translates to:
// type CapitalizeStringType = String
The OmitThisParameter
utility type does the opposite of the previous type. It takes a function type as an argument, via the Type
, and returns the function type without this
parameter.
// Create a function with this parameter:
function capitalize(this: String) {
return this[0].toUpperCase + this.substring(1).toLowerCase()
}
// Create type based on capitalizeString function:
type CapitalizeType = OmitThisParameter<typeof capitalize>
// Translates to:
// type CapitalizeType = () => string
// Create a function without this parameter:
function sayHi(name: string) {
return `Hello, ${name}.`
}
// Create type based on sayHi function:
type SayHiType = OmitThisParameter<typeof sayHi>
// Translates to:
// type SayHiType = (name: String) => string
The ThisType
utility type allows you to explicitly set the this
context. You can use it to set this
for whole object literals or just a single function. Before you try this, make sure you have the compiler flag —-noImplicitThis
enabled.
// Create interface for this in user object:
interface User {
username: string;
email: string;
isActivated: boolean;
printUserName(): string;
printEmail(): string;
printStatus(): boolean;
}
// Create user object and set this type to User interface:
const userObj: ThisType<User> = {
username: 'Andreas',
email: '[email protected]',
isActivated: false,
printUserName() {
return this.username
},
printEmail() {
return this.email
},
printStatus() {
return this.isActivated
}
}
One nice thing on TypeScript built-in utility types is that you can also combine them. You can combine one utility type with another utility type. You can also combine one utility type with other types. For example, you can combine types with union or intersection types. This allows you to create even more advanced types.
// Create User interface:
interface User {
username: string;
password: string;
}
// Create SuperUser interface:
interface SuperUser {
clearanceLevel: string;
accesses: string[];
}
// Create type RegularUser by combining User and SuperUser.
// Make properties in User required
// and properties in SuperUser optional:
type RegularUser = Required<User> & Partial<SuperUser>
// This is valid:
const joeJoe: RegularUser = {
username: 'joejoe',
password: 'some_secret_password_unlike_123456',
}
// Also valid:
const jane: RegularUser = {
username: 'jane',
password: 'foo_bar_usually-doesnt_work-that_well',
clearanceLevel: 'A'
}
// This will not work:
const pietro: RegularUser = {
username: 'pietro'
}
// TS error: Type '{ username: string; }' is not assignable to type 'RegularUser'.
// Property 'password' is missing in type '{ username: string; }' but required in type 'Required<User>'.(
These were the last eight built-in utility types in TypeScript. As you can see, these utility types can help you get a lot of work with types done. This, while making it easier for you and doing all the heavy lifting. I hope that this article helped you understand how these built-in utility types work and how to use them.
27