Exploring the TypeScript Features for Enhanced Coding Efficiency

November 9, 2023 4 min read

Nowadays, it’s difficult to imagine a serious JavaScript-based application without a TypeScript superset. Interfaces, tuples, generics, and other features are well-known among TypeScript developers. While some advanced constructs may require a learning curve, they can significantly bolster your type safety. This article aims to introduce you to some of these advanced features.

Type Guards

Type guards help us to get info about a type within a conditional block. There are a few simple ways to check the type using intypeofinstanceof operators, or using equality comparison (===).

In this section, I’d like to pay more attention to user-defined type guards. This guard serves as a simple function that returns a boolean value. In other words, the return value is a type predicate.
Let’s take a look at the example when we have base user info and user with additional details:

type User = { name: string };

type DetailedUser = { 

  name: string; 

  profile: { 

    birthday: string

  }

}

function isDetailedUser(user: User | DetailedUser) {

  return ‘profile’ in user;

}

function showDetails(user: User | DetailedUser) {

    if (isDetailedUser(user)) {

        console.log(user.profile); // Error: Property ‘profile’ does not exist on type ‘User | DetailedUser’.

    }

}

The isDetailedUser function returns a boolean value, but it does not identify this function as a boolean that “defines the object type.”

In order to achieve the desired result, we need a little bit of update isDetailedUser function using “user is DetailedUser” construction

function isDetailedUser(user: User | DetailedUser): user is DetailedUser {

  return ‘profile’ in user;

}

Indexed Access Types

There may be the case in your app when you have a large object type and you want to create a new type, that uses a part of the original one. For example, part of our app requires only a user profile. User[‘profile’] extracts the desired type and assigns it to the UserProfile type.

type User = {

  id: string;

  name: string;

  surname: string;

  profile: {

    birthday: string;

  }

}

 

type UserProfile = User[‘profile’];

What if we want to create a type based on a few properties? In this case, you can use a built-in type called Pick.

type FullName = Pick<User, ‘name’ | ‘surname’>; // { name: string; surname: string }

There are many other utility types, such as OmitExclude, and Extract, which may be helpful for your app. At first sight, all of them are kind of indexed types, but actually, they are built on Mapped types.

Indexed Types With an Array

You might have met the case when an app provided you with a union type, such as:
type UserRoleType = ‘admin’ | ‘user’ | ‘newcomer’;

Then, in another part of the app, we fetch user data and check its role. For this case, we need to create an array:

const ROLES: UserRoleType[] = [‘admin’, ‘user’, ‘newcomer’];

ROLES.includes(response.user_role);

 

Looks tiring, doesn’t it? We need to repeat union-type values inside our array. It would be great to have a feature to retrieve a type from an existing array to avoid duplication. Fortunately, indexed types help here as well.

First of all, we need to declare our array using a const assertion to remove the duplication and make a read-only tuple.

const ROLES = [‘admin’, ‘user’, ‘newcomer’] as const;

Then, using the typeof operator and number type, we create a union type based on the array value.

type RolesType = typeof ROLES[number]; // ‘admin’ | ‘‘user’ | ‘‘newcomer’;

You may be confused about this solution, but as you may know, arrays are object-based constructions with numeric keys. That’s why, in this example, number is used as the index access type.

Conditional Types and Infer Keyword

Conditional types define a type that depends on the condition. Usually, they are used along with generics. Depending on the generic type (input type), construction chooses the output type.

For example, the built-in NonNullable TypeScript type is built on conditional types.

type NonNullable<T> = T extends null | undefined ? never : T

type One = NonNullable<number>; // number

type Two = NonNullable<undefined>; // never

 

The infer keyword is used with conditional types and can not be used outside of the ‘extends’ clause. It serves as a ‘type variable creator.’

I think it will be easier for you to understand it by looking at the real example.

Case: retrieve async function result type.

const fetchUser = (): Promise<{ name: string }> => { /* implementation */ }

The easiest solution is to import the type declaration and assign it to the variable. Unfortunately, there are cases when result declaration is written inside the function, as in the example above.

This problem may be resolved in two steps:

  1.  The Awaited utility type was introduced in TypeScript 4.5. For learning purposes, let’s look at the simplified variant.export type Awaited<T> = T extends Promise<infer U> ? U : T;

     

    Using conditional types and infer keyword, we “pull out” the promised type and assign it to the Uname. It’s a kind of type variable declaration. If the passed type is acceptable with PromiseLike generic, construction returns the original type saved to the U name.

  2.  Get value from the async function.

    Using built-in ReturnType that extracts the return type of function and our Awaited type, we achieve the desired result:

    export type AwaitedReturnType<T> = Awaited<ReturnType<T>>;

     

I hope you found this article useful for yourself. Have fun coding!

Originally published on DZone.

Don't See Your Dream Job?