Contact
Stories

Development

5 min read

Typescript: Tips and Tricks for Improving Your Coding Skills [Part 1]

Mihael
Mihael
Web Development

Do you want to improve your coding experience and understand Typescript on a deeper level? Yes? Then continue reading. In this article, I will introduce you to a collection of some less known and advanced tips, tricks, and possibilities of Typescript. But before I go on, let’s define Typescript first.

What is Typescript? 

In a nutshell, Typescript is Javascript with types. A more sophisticated description would be that it is an open-source language that builds on JavaScript by adding type definitions. Think of it as an advanced and upgraded JavaScript. But keep in mind, you do not have to use Typescript to its full extent - although I strongly suggest you do. 

Now that we have recalled the definition of Typescript, let's get down to business. I will explain utility and conditional types through examples and give you solutions for possible coding problems. If you have never worked with Typescript, please come back to this article if you ever decide to give it a try. 

1. You can index types and interfaces

Don't miss out on this! Indexing interfaces/types is a very simple, yet useful feature. It allows you to select a type as you would select a property of an object. I'm sure you're already familiar with it, but in case you're not, I have to mention that knowledge of it will help you further down the line of this article.

Take a look at the following code:

interface User {
 name: string;
 lastName: string;
 dateOfBirth: {
   day: number;
   month: number;
   year: number;
 };
}
// This is valid code
const someDateOfBirth: User["dateOfBirth"] = {
 day: 2,
 month: 3,
 year: 4,
};

The variable “someDateOfBirth” has a type that we took from the “User” interface.

2. Utility types 

On the official Typescript website, you can find a list full of utility types documentation pages. If you`re not familiar with them, I recommend you to read them. There are also some scenarios in this chapter on which I would like to focus.

2.1 Pick - Take care of what you actually pick

The “Pick” utility is really easy to understand. It picks up only those properties from the interface you specified and you can create a new type including the chosen properties.

Here is an example from the “User” interface, where we searched for “name” and “lastName”:

interface User {
 name: string;
 lastName: string;
 height: number;
 email?: string;
}
// This is valid code, a “picked” User
const userWithFullNameOnly: Pick<User, "name" | "lastName"> = {
 name: "John",
 lastName: "Doe",
};

It looks all nice, but now imagine a function like this;

function mailToUserIfEmailExists(user: Pick<User, "name" | "email">) {
 if (user.email) {
   sendMail(user.email, `Hello ${name}, ......`);
 }
}

If you take a look again at the interface, you will notice an issue - the “email” property is an optional property, marked with “?” , which means “Pick” will also “pick up” that “optionality”.

So this code would do the job:

const forEmail: Pick<User, "name" | "email"> = {
 name: "John",
};
// This passes
mailToUserIfEmailExists(forEmail);

The problem with this code is that we will never send this “John” user an email. Luckily, there is a way to fix this, so I will give you three possible solutions for this error:

  1. Use “Required” utility: this is also one of the simple utilities because it makes all fields of your interface required (not undefined):
function mailToUserIfEmailExists(user: Required<Pick<User, "name" | "email">>) {
 if (user.email) {
   sendMail(user.email, `Hello ${name}, ......`);
 }
}
const forEmail: Pick<User, "name" | "email"> = {
 name: "John",
};
// This now does not pass, will throw error at build time
mailToUserIfEmailExists(forEmail);

It will force you to make sure that the user has an email before passing it to the function. After that, you will need to remove the “if” check because now you know that the user.email will always be defined. When your function has a pattern like "doSomethingIfOtherThing", you should rethink your approach. I intentionally went with this example to show you that a condition like this would never be false.

  1. You can use indexing I mentioned earlier - it will make the property required:
function mailToUserIfEmailExists(user: {
 name: User["name"];
 email: User["email"];
}) {
 if (user.email) {
   sendMail(user.email, `Hello ${name}, ......`);
 }
}
const forEmail: Pick<User, "name" | "email"> = {
 name: "John",
};
// This now does not pass, will throw error at build time
mailToUserIfEmailExists(forEmail);

Note this - unlike the “Required” approach, you can make it possible to pass a user with an undefined value. It will force you to have that property even though it is unspecified. Take a look at this code:

function mailToUserIfEmailExists(user: {
 name: User["name"];
 email: User["email"];
}) {
 if (user.email) {
   sendMail(user.email, `Hello ${name}, ......`);
 }
}
const forEmail: { name: User["name"]; email: User["email"] } = {
 name: "John",
 email: undefined,
};
// This will pass even though email is undefined
mailToUserIfEmailExists(forEmail);
  1. Shortcut for solutions 1 and 2

You can use this type to repeat the first solution (email can never be undefined) with less code repetition:

type PickRequire<T, K extends keyof T> = Required<Pick<T, K>>;

// Your function would start like this then
function mailToUserIfEmailExists(user: PickRequire<User, "name" | "email">);

Or you can use this code to repeat the second solution (email may be undefined, but you will further emphasize this) with less code repetition:

type PickRequireKeys<T, K extends keyof T> = {
 [P in keyof Required<Pick<T, K>>]: T[P];
};

// Your function would start like this then
function mailToUserIfEmailExists(user: PickRequireKeys<User, "name" | "email">);

If you still have no idea what is going on here, don’t worry. Continue reading and I will explain everything later in the article.

2.2 Pick - When you pick, you do not have to pick everything

Let’s say you have this function:

function printUserFullName(user: Pick<User, "name" | "lastName">) {
 print(`${user.name} ${user.lastName}`);
}

Here, you don't have to pass the picked object to the function if you want to call it. So, don't do this:

const user: User = {
 name: "John",
 lastName: "Doe",
 height: 175,
 email: "john.doe@foo.bar.doe",
};
const pickedUser: Pick<User, "name" | "lastName"> = {
 name: user.name,
 lastName: user.lastName,
};
printUserFullName(pickedUser);

Instead, you can just pass in full user:

const user: User = {
name: 'John',
lastName: 'Doe',
height: 175,
email: 'john.doe@foo.bar.doe',
};
// Works fine
printUserFullName(user);

This only works if you are supplying a “wider” interface than what is required, although even the “smaller” one doesn't have to be derived via “Pick”. But watch out! It will also work if you provide a completely different type, but “name” and “lastName” must match.

So here is the complete code:

interface User {
 name: string;
 lastName: string;
 height: number;
 email?: string;
}
interface ExternalUser {
 // This first two properties are same as on "User"
 name: string;
 lastName: string;
 company: string;
 position: string;
}
const externalUser: ExternalUser = {
 name: "John",
 lastName: "Doe",
 company: "Foo",
 position: "Bar",
};
// Works fine even though we picked from "User"
printUserFullName(externalUser);
function printUserFullName(user: Pick<User, "name" | "lastName">) {
 print(`${user.name} ${user.lastName}`);
}

Imagine a scenario where you don't want a function to do anything with "externalUser" - it would be better to avoid “Pick” and demand a full type. Although this is not an issue most of the time, it comes in handy.

2.3 Record - watch out when using string | number | symbol

Record utility allows us to create an object with predefined keys and values. Check this out:

const productFriendlyNames: Record<"keyboard" | "mouse", string> = {
 keyboard: "Brand new keyboard",
 mouse: "XYZ Gaming mouse",
};

The issue appears when we use a “less specific” type for the keys (first parameter of Record) such as “string”. Here is an example:

const functionsDictionary: Record<string, () => void> = {
 foo: () => {},
};
// This passes compilation, will crash at run time
functionsDictionary.bar();

An issue with this function is that functionsDictionary.bar doesn't exist, but as far as Typescript is concerned, we could call it.

Okay, now what would be the solution here? Well, there are some advocates to make “Record” properties possibly undefined by default. If you need that kind of behavior, you can create it yourself using the “Partial” utility type. “Partial” is the opposite of “Required”. Why? Because it makes all fields optional ergo, maybe undefined. It will force you to treat every possible key as maybe not set. So, the code would look like this:

const functionsDictionary: Partial<Record<string, () => void>> = {
 foo: () => {},
};
// This now won't pass compilation
functionsDictionary.bar();
// We must write it like this now, this passes compilation and does not crash application at run time
functionsDictionary.bar?.();

Note that you have a similar issue with arrays:

const arr: Array<() => void> = [];
// This passes compilation but will crash at run time
arr[0]();

There is a long debate on this within the Typescript community if this should be the default behavior. One argument is that you should know the length of your array before indexing, but this is something I will leave for a future article.

2.4 The third solution for the “Pick” problem

Okay, remember this code from a bit earlier?

type PickRequireKeys<T, K extends keyof T> = {
 [P in keyof Required<Pick<T, K>>]: T[P];
};

Here is a quick overview of things that are used for creating this type:

  • Generics
  • keyof
  • Mapped type
  • Utilities I mentioned above
  • Indexing (the first thing explained in this article)

Let's move on to explaining each one of the above-mentioned topics. I'll keep it short, promise. 

Generics

As the name suggests, they allow you to take a generic type and do something with it - the Pick, Require, and Record types I used above are generics. This is the most basic example of it:

type Maybe<T> = T | null | undefined;

// All three work
const foo: Maybe<string> = "";
const foo1: Maybe<string> = null;
const foo2: Maybe<string> = undefined;

It is the equivalent to writing:

const foo: string | null | undefined

for each of the foos.

keyof

Very simple: it takes all keys of a given type:

interface Car {
 brand: string;
 color: string;
}

type CarKeys = keyof Car; // equal to: 'brand' | 'color'

Mapped type:

Simply said, “Mapped type” creates a type where both keys and values are predefined: 

type PriceMap = {
 [product: string]: number;
};

const prices: PriceMap = {
 banana: 4,
 apple: 1,
};

You may have noticed the in a keyword and slightly different syntax:

[P in keyof Required<Pick<T, K>>]

Essentially, what I am doing here is defining what the type of value each property P of  Required<Pick<T, K>> should be.


Let me explain the solution as simple as possible. Firstly, I created a new type that takes an interface and keys you want to “pick” properties from. That type works by passing the interface through Required, which will remove the optionality from the key. In the next and last step, we will just index the T with a P. It will pick up the type directly from the T and not from the Required<Pick<T, K>>, so the undefined possibility is still being preserved while optionality is not.


Let’s take five 

I know it’s a lot of new information, so let's take a break here. So far, you have learned what Pick utility can do, how to fix the “Pick” problems, how to use the string | number | symbol in Record utility, and much more. In the second part of this article, I will show you how to make types change depending on some conditional types and explain how to work with type unions to leverage things you can do with them. Stay tuned. :) 

Like what you just read?

Go on, spread the news!

About the author

Mihael is a Frontend Web developer at COBE. When he’s not mastering his frontend skills in React and TypeScript, he's either reading books or watching movies.

Mihael

Web Development

Write
Mihael
Write COBE
Related Stories

This was interesting to you? Check these out.