Typescript features that will significantly upgrade your skills

Andrew Erokhin
6 min readDec 10, 2023

Discover a handful of intriguing Typescript features that will add value to your projects. Perhaps some of them you’ve always wanted to learn, finding them too complex, and others you wish you knew but didn’t realize they even existed. In this article, we’ll explore techniques that can significantly boost your skills with minimal effort.

1 — Satisfies

How to ensure that a variable matches a specific type? Typically, we would use an explicit assignment, as shown in the example below. Let’s imagine that we have an object with paths. Some of them are simple strings, while others are functions that return strings.

type PathKey = 'posts' | 'postItem' | 'statistics'
type PathFunction = (...args: any[]) => string
type PathValue = PathFunction | string

const paths: Record<PathKey, PathValue> = {
posts: '/posts',
postItem: (id: number): string => `/posts/${id}`,
statistics: '/statistics',
}

At first glance, that code may seem to be working correctly, but let’s try calling a property of the paths object that contains a function.

We can see that we are getting a ts error because Typescript doesn’t know exactly what is in an object’s property (string or function) when we assign a type Record<PathKey, PathValue> to the paths object. To resolve this issue, we need to have a type for our object that will describe each key, which is the default behavior of a const in ts. So, how do we achieve having a type with exact keys and check the type correctness of our paths object at the same time? Here, satisfies operator comes to the rescue.

/*
const paths: {
posts: string
postItem: (id: number) => string
statistics: string
}
*/
const paths = {
posts: '/posts',
postItem: (id: number): string => `/posts/${id}`,
statistics: '/statistics',
} satisfies Record<PathKey, PathValue>

We can see that we no longer encounter the error when we are trying to refer to the property.

paths.postItem(3) // ✅ no error

Let’s try to add an undescribed path to the path object.

const paths = {
posts: '/posts',
postItem: (id: number): string => `/posts/${id}`,
statistics: '/statistics',
undescribedRoute: '/undescribed-route',
} satisfies Record<PathKey, PathValue>

This will result in the following error:

Now we have the same type checking for path as if we had an explicit assignment, and we can use any of its properties without encountering ts errors! Isn’t that perfect?

2 — Infer

In some scenarios, it’s necessary to extract types from other types. This doesn’t happen frequently, but understanding it helps us build more complex structures properly. One such tool in Typescript is the infer keyword, which proves valuable in scenarios where we need to derive types, such as when dealing with promises and API responses.

Extracting types from promises

Let’s begin with a straightforward example. Assume we want to capture the return type of a function that returns a Promise. The infer operator can be used as follows:

type ExtractFromPromise<T> = T extends () => Promise<infer R> ? R : never
// type ExtractedType = number
type ExtractedType = ExtractFromPromise<() => Promise<number>>

Let’s break down what is in the code above. Here, ExtractFromPromise checks if the provided type T is a function returning a Promise, using the ternary operator. If so, it extracts the type (R) from the Promise; otherwise, it defaults to never. This mechanism allows us to manipulate the extracted type as we wish.

Creating function types

Building on this, we can create a function type based on the extracted type. For example, we can create a new function type that receives an array of the extracted type as a parameter.

type CreateFunctionType<T> = T extends () => Promise<infer R>
? (arg1: R[]) => string
: never
// type Type = (arg1: number[]) => string
type Type = CreateFunctionType<() => Promise<number>>

3 — Exhaustive type checking

Wouldn’t it be ideal if we could spot a bug during development rather than runtime? Typescript was precisely designed for this purpose. In this section, we’ll explore how to harness its power for comprehensive type checking.

Usage in functions

Let’s consider a simple object of type Order with 3 possible statuses: inactive, in progress, and completed.

enum OrderStatus {
Inactive,
InProgress,
Completed,
}

interface Order {
status: OrderStatus
name: string
}

const inactiveOrder: Order = {
status: OrderStatus.Inactive,
name: 'John',
}

Now, let’s consider a scenario where we need to create a function that will return a text based on the status property

const getTextStatus = (order: Order) => {
switch (order.status) {
case OrderStatus.Inactive:
return `${order.name}, your order is inactive!`
case OrderStatus.InProgress:
return `${order.name}, your order is in progress.`
case OrderStatus.Completed:
return `${order.name}, your order is completed.`
default:
return ''
}
}

Imagine that we added a new status Removed to the OrderStatus enum and created a removedOrder object.

enum OrderStatus {
// ...
Removed,
}

const removedOrder: Order = {
status: OrderStatus.Removed,
name: 'John',
}

What will getTextStatus return if we pass removedOrder into it?

Correct, we will receive an empty string instead of text, as we don’t have the Removed status handled inside the function.

const removedOrderStatusText = getTextStatus(removedOrder) // ''

Let’s explore how we can prevent this scenario by leveraging an exhaustive check.

const getTextStatus = (order: Order) => {
switch (order.status) {
case OrderStatus.Inactive:
return `${order.name}, your order is inactive!`
case OrderStatus.InProgress:
return `${order.name}, your order is in progress.`
case OrderStatus.Completed:
return `${order.name}, your order is completed.`
default:
const _exhaustiveCheck: never = order.status // 🚫 ERROR! Type `OrderStatus` is not assignable to type `never`
}
}

In the code above, we explicitly assign the type never to the created _exhaustiveCheck const. This results in an error as we haven’t handled all possible statuses.

Let’s handle the Removed status:

const getTextStatus = (order: Order) => {
switch (order.status) {
case OrderStatus.Inactive:
return `${order.name}, your order is inactive!`
case OrderStatus.InProgress:
return `${order.name}, your order is in progress.`
case OrderStatus.Completed:
return `${order.name}, your order is completed.`
case OrderStatus.Removed:
return `${order.name}, your order is removed.`
default:
const _exhaustiveCheck: never = order.status // ✅ no error, all values handled.
}
}

Now, we can catch a bug immediately as we write our code!

Usage in React components

We can apply the same technique to any function, including a React component! Let’s delve into it.

Imagine a React component that dynamically renders different components based on a post.type property passed into it.

enum PostType {
Basic,
ImageGallery,
Video,
}

interface Post {
type: PostType
}

interface PostProps {
post: Post
}

const PostItem: FC<PostProps> = ({ post }) => {
switch (post.type) {
case PostType.Basic:
return <>Basic post</>
case PostType.ImageGallery:
return <>Image gallery post</>
case PostType.Video:
return <>video post</>
default:
const _exhaustiveCheck: never = post.type
return null
}
}

Here we observe a common approach for handling conditional rendering in a React component. Normally, real components like <BasicPost /> or <VideoPost /> would be used in the switch statement, each with its own structure. For simplicity, we return a fragment with text.

Also, since there’s a good chance we’ll want to reuse this trick in different areas of our app, we can extract an exhaustive check into a function like this:

function exhaustiveCheck(param: never) {}

const PostItem: FC<PostProps> = ({ post }) => {
switch (post.type) {
case PostType.Basic:
return <>Basic post</>
case PostType.ImageGallery:
return <>Image gallery post</>
case PostType.Video:
return <>video post</>
default:
exhaustiveCheck(post.type)
return null
}
}

Conclusion

In this article, we delved into three fantastic typescript techniques that will inevitably enhance your projects’ codebase.

Thanks for reading! I hope you found this article interesting and useful! If you have any suggestions or questions, please feel free to leave your comments.

--

--