sudolabs logo

17. 4. 2024

4 min read

Typescript Tapas

TypeScript is an incredible tool, but to fully benefit from its capabilities, you need to be aware of its full potential. Writing well-typed code is a superpower in itself – sometimes, with just minor changes to your types, you can significantly enhance the safety and maintainability of your code. We've compiled some real-life examples to demonstrate how code can perform better with improved typing. These code snippets may be small, but tasty – much like some good tapas.

Oliver Dendis

Software Engineer

In this article, we will show practical applications of template literal types, effective utilization of type predicates, and tips for type narrowing for consistently obtaining accurate types.

1. Don’t hide any rules to typescript

When working with React or Next.js routers, I've frequently noticed the convenience of storing route names and paths in constants or enums. Typically, it appears like this:

const PageRoutes = {
Home: "/",
SignIn: "/sign-in",
Error: "/error"
}
// ...later in code
const navigateToHomePage = () => {
router.push(PageRoutes.Home)
}

It seems straightforward, as TypeScript is capable of inferring the type of routes on its own. However, in the code above, there are some implicit rules:

  • All paths must start with "/".

  • All keys of the PageRoutes object start with a capitalized letter.

Therefore, if someone (and yes, it could even be you!) breaks these rules, there won't be any complaints.

const PageRoutes = {
Home: "/",
SignIn: "/sign-in",
Error: "/error",
signUp: "signup" // new signup page, but with missing "/"
}

If you don't have a test for routing to the sign-up page, you might overlook bugs in incorrect routing. Additionally, if you have conventions or rules in place, it's essential to reflect these in your types.

To address the first issue, you can create a type for the PageRoute object using template literal types. This enables you to incorporate template literal strings into your type definitions using the same syntax as JavaScript.

type PageRoutes = Record<string, `/${string}`>
const PageRoutes = {
Home: "/",
SignIn: "/sign-in",
Error: "/error",
signUp: "signup" // TS is complaining now - Type '"signup"' is not assignable to type '`/${string}`'
} as const satisfies PageRoutes

Since I'm using capitalized names for routes, I can also reflect this in my types using the Capitalize utility type in TypeScript.

type PageRoutes = Record<Capitalize<string>, `/${string}`>

Even though I've fixed the missing "/" in the sign-up page path, I can still see TypeScript correctly complaining:

type PageRoutes = Record<Capitalize<string>, `/${string}`>
const PageRoutes = {
Home: "/",
SignIn: "/sign-in",
Error: "/error",
signUp: "/signup" // TS is complaining now - Object literal may only specify known properties, and 'signUp' does not exist in type 'PageRoutes'.
} as const satisfies PageRoutes

Don't hesitate to narrow your types if you have some less obvious rules. With simple template literals, you can safeguard your functionality and conventions.

Code playground

2. Help Typescript to know your types

Another common scenario in code is having arrays of items where some of the items can be nullable or undefined. If I want to retrieve only non-falsy values, I can use the Array.filter function:

const items = [{name: "Sudolabs"}, null, undefined, {name: "Typescript"}]
const filteredItems = items.filter(Boolean).map(item => item.name)

This works perfectly on the code level. All falsy values are filtered out. However, for some reason, TypeScript is still complaining when hovering over the item in the Array.map function.

'item' is possibly 'null' or 'undefined'.

In this situation, TypeScript needs assistance in knowing that the filtered values are no longer present in the array. This is where a Type Predicate comes into play. A Type Predicate should be used as the return type of a function in which we check if some value matches the interface or not. In this case, we need to include a type predicate in our Boolean function to inform TypeScript that the value is not null or undefined. It will look like this:

type Item = {
name: string
}
const filterItems = items.filter((item): item is Item => Boolean(item)).map(item => item.name)

We can even move it to a separate function:

const items = [{name: "Sudolabs"}, null, undefined, {name: "Typescript"}]
type Item = {
name: string
}
const isItemDefined = (item: Item | null | undefined): item is Item => Boolean(item)
const filterItems = items.filter(isItemDefined).map(item => item.name)

By introducing generics, we can make this function universally applicable for filtering out falsy values:

const isDefined = <T,>(item: T | null | undefined): item is T => !!item
const items = [{name: "Sudolabs"}, null, undefined, {name: "Typescript"}]
const filterItems = items.filter(isDefined).map(item => item.name)

Code playground

3. Make the Typescript precise

Imagine a scenario where you have two types of users in your system: one for suppliers and one for customers.

type Customer = {
id: string,
role: "customer"
firstName: string,
lastName: string
}
type Supplier = {
id: string,
role: "supplier",
businessName: string
}
type User = Customer | Supplier

If you want to retrieve only users with specific roles, you can create the function getUsersByRole like this:

const users: User[] = [{id: "1", role: "customer", firstName: "Oliver", lastName: "Dendis"}, {id: "2", role: "customer", firstName: "Foo", lastName: "Bar"}, {id: "3", role: "supplier",businessName: "Sudolabs"}]
const getUsersByRole = (role: 'customer' | "role") => {
return users.filter(user => user.role === role)
}
const customers = getUsersByRole('customer') // customers is type Users[]

It works perfectly! Your function now returns only users with the role "customer". Now, you can be confident that all users have a first name and last name. However, TypeScript will not allow this.

const customerNames = customers.map(customer => customer.firstName) // Property 'firstName' does not exist on type 'User'. Property 'firstName' does not exist on type 'Supplier'.

The thing is that Typescript inferred the type of customers as User[] , even though we know it should be Customer[]. In this situation, we know more than Typescript and that’s awesome because it means that there are still some fields where humans can beat the machines!

In this situation, we need to make the Typescript precise. To address this, let's introduce some utility types. We need a utility that returns the correct type (Customer or Supplier) based on the provided role. It can be implemented like this:

type GetUser<T extends User["role"]> = Extract<User, { role: T }>
type Foo = GetUser<'customer'> // customer type
type Bar = GetUser<'supplier'> // supplier type

With such a utility, we can create a better-typed version of getUsersByRole. By specifying the return type of the function based on its role using the GetUser utility type, we can enhance our getUsersByRole function.

const getUsersByRole = <T extends User['role']>(role: T) => {
return users.filter((user): user is Extract<User, { role: T }> => user.role === role)
}

In this case, TypeScript infers types correctly, and there are no longer any complaints:

const customers = getUsersByRole('customer') // customers is type Customer[]
const customerNames = customers.map(customer => customer.firstName) // TS is happy!

Code playground

Wrapping up

Here, we explored some lesser-known features of TypeScript that significantly enhance code quality and type safety. The art of TypeScript lies in writing types as narrow as possible yet general enough to be reusable across multiple use cases. Through three illustrative examples, we showcased how minor adjustments in code, such as TypeScript utility types and type predicates, can help TypeScript robustly safeguard our functionality. We will continue in the next article!

Share

Let's start a partnership together.

Let's talk

Our basecamp

700 N San Vicente Blvd, Los Angeles, CA 90069

Follow us


© 2023 Sudolabs

Privacy policy
Footer Logo

We use cookies to optimize your website experience. Do you consent to these cookies and processing of personal data ?