Mindblowing ๐Ÿคฏ TypeScript tricks

Mindblowing ๐Ÿคฏ TypeScript tricks

ยท

15 min read

Read in light, dark or mid-day theme on my blog

Apologies for the clickbaity title ๐Ÿ™ƒ. But it is in good faith, cuz I'm gonna introduce you to some TypeScript related tricks that are bound to blow your mind to pieces. And if you can read the whole post without feeling wonder at any trick, great for you!! You're TypeScript pro already ๐Ÿฅณ

So let's cut to the chase.

A little note...

The level of this article is Advanced. You may not understand how things work. However, you don't have to. You only have to copy paste the snippet, and understand how to use it, as these will make your life easy, and overtime, you'll get the know-how of how these actually work.

In-built types ๐Ÿฅฑ

These are some of the built-in helper types in TypeScript. I'll keep this section short, as you can read about these anywhere. A good starting point would be TypeScript Docs Then we'll get to the juicy stuff ๐Ÿ˜‹

Pick

It allows to pick specific fields from a type/interface, along with their types and create a brand new type. Let's take a look at this ๐Ÿ‘‡

type UserFields = {
  id: number;
  name: string;
  gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
  dob: Date;
};

type NameAndGenderOnly = Pick<UserFields, 'name' | 'gender'>;

// This is equal to ๐Ÿ‘‡
type NameAndGenderOnly = {
  name: string;
  gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
};

See!?! The same types, without any duplication.

Partial

This is the most used type of mine. If you have a type/interface, and for some reason, you wanna make all its fields optional, this is it ๐Ÿ‘‡

type UserFields = {
  id: number;
  name: string;
  gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
  dob: Date;
};

type OptionalUserFields = Partial<UserFields>;

// This is equal to ๐Ÿ‘‡
type OptionalUserFields = {
  id?: number;
  name?: string;
  gender?: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
  dob?: Date;
};

Readonly

This is very useful, when you wanna make sure that an object's properties can't be changed in your code. Think of it as a const for your object properties.

type UserFields = {
  id: number;
  name: string;
  gender: 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
  dob: Date;
};

const userData: Readonly<UserFields> = {
  id: 100,
  name: 'Puru Vijay',
  gender: 'male',
  dob: new Date('12 Nov, 2001'),
};

Trying to modify any property like userData.name = 'Hoolalala' will result in error.

Record

Now we are getting to the good stuff. I've had a new-found respect for Record recently, while working on my current project macos.now.sh (Shameless Plug, It's basically a macOS Big Sur clone written in Preact and Vite).

Take a look at this ๐Ÿ‘‡

export type AppName =
  | 'finder'
  | 'launchpad'
  | 'safari'
  | 'messages'
  | 'mail'
  | 'maps'
  | 'photos'
  | 'facetime'
  | 'calendar';

/** Which apps are currently open */
export const openApps: Record<AppName, boolean> = {
  finder: false,
  launchpad: false,
  safari: false,
  messages: false,
  mail: false,
  maps: false,
  photos: false,
  facetime: false,
  calendar: false,
};

As you can see, this is just a simple key-value pair. But I wanted to enforce that this object contains all the apps listed in the AppName union type, and that all the values are boolean only. I also wanted to be presented with an error if I add a new app to the list, which would make me add that app's key value pair to this openApps object.

This is where Record comes in. It's simply a way to enforce the types of the keys as well as values. Another layer of safety that TypeScript adds.

Juicy stuff ๐Ÿ˜‹

Now the fun part begins.

Retrieve element type from Array

Suppose you have an Array, and you wanna extract the type of each Element from an array

type ArrayElement<
  ArrayType extends readonly unknown[]
> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;

We're using TypeScript's infer here, which helps pick out specific types from a complex type.

Here's how to use it:

type A = ArrayElement<string[]>; // string
type B = ArrayElement<readonly string[]>; // string
type C = ArrayElement<[string, number]>; // string | number
type D = ArrayElement<['foo', 'bar']>; // "foo" | "bar"
type E = ArrayElement<(P | Q | R)[]>; // P | Q | R

type Error1 = ArrayElement<{ name: string }>;
//                         ^^^^^^^^^^^^^^^^
// Error: Type '{ name: string; }' does not satisfy the constraint 'readonly unknown[]'.

There's a bit simpler version to get the element type.

type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType[number];

Retrieve type from a promise

Ever wanted to retrieve type from a function that returns a promise? You might've tried this:

function returnsPromise(): Promise<number>;

let num: typeof returnsPromise;
//       ^^^^^^^^^^^^^^^^^^^^^
// num: () => Promise<number>

We want num's type to be the returned type of the promise(in this case number), and the above solution definitely didn't work.

The solution is to once again use infer to retrieve the type from the promise:

type UnwrapPromise<T> = T extends (props: any) => PromiseLike<infer U>
  ? U
  : T extends PromiseLike<infer K>
  ? K
  : T;

usage:

function returnsPromise(props: any) {
  return Promise.resolve(6);
}

const num: UnwrapPromise<typeof returnsPromise> = 8;
//    num: number

Here we wrapped a function that returns a promise into this type. This works directly with a regular Promise<unknown> type too.

Why PromiseLike instead of Promise? \

Promise interface comes with lot of pre-built methods exclusive to promises. But sometimes, you wanna create functions that return a .then just like Promises, but not have all the properties that Promises do. In that case, we use PromiseLike

Aside: You could rename UnwrapPromise to be BreakPromise. Doesn't affect the code, but its good for laughs ๐Ÿคฃ๐Ÿคฃ

Turning a tuple into union types

This is a tuple:

const alphabets = ['a', 'b', 'c', 'd'] as const;

Note: Without as const at the end, typescript will interpret the type as string[], not as a tuple

Now we want to use these specific strings as union types. Easy peasy.

type Alphabet = 'a' | 'b' | 'c' | 'd';

This will do. But let's assume that this type and the array above are gonna end up in different files, and the project grows quite big, then you come back a few months later, and add another value e to the alphabets variable, and BOOM!!! The whole codebase breaks, because you forgot to add e in the Alphabet union type.

We can automate the Alphabet union type generation, in such a way that it pulls its members directly from alphabets variable.

type Alphabet = typeof alphabets[number];

And here's the universal type safe helper:

type UnionFromTuple<Tuple extends readonly (string | number | boolean)[]> = Tuple[number];

Usage:

const alphabets = ['a', 'b', 'c', 'd'] as const;

type Alphabet = UnionFromTuple<typeof alphabets>;
//  type Alphabet = 'a' | 'b' | 'c' | 'd'

Why readonly array? \ \ This section is about Tuple to Union types, but in the code itself we haven't used the word tuple. The reason is that tuple isn't a keyword. As far as TypeScript is concerned, a readonly Array is a tuple. There's no Tuple type or anything. That's why I'm making sure the type passed to UnionFromTuple is a tuple, not an array. If its an array, its basically the same as the section above where we retrieved the element type from an array

Union types from object

Let's say we have this object:

const openApps = {
  finder: false,
  launchpad: false,
  safari: false,
  messages: false,
  mail: false,
  maps: false,
  photos: false,
  facetime: false,
  calendar: false,
};

And I want to create a union type that's based on the keys specified here. If I add an extra key-value pair to this object, I want the union type to include that too.

Here's the solution:

type KeysOfObject<T extends { [K in string | number]: unknown }> = keyof T;

Usage๐Ÿ‘‡

type App = KeysOfObject<typeof openApps>;

This will be equal to ๐Ÿ‘‡

type App =
  | 'finder'
  | 'launchpad'
  | 'safari'
  | 'messages'
  | 'mail'
  | 'maps'
  | 'photos'
  | 'facetime'
  | 'calendar';

A better Object.Keys

Looking the article, it seems like its a compilation of Helper Types, which is the case. But in this one, I'm gonna share a tip which isn't the most mind-blowing or the coolest. Its pretty boring, but the important thing is that it's the most MOST useful tip in this whole article. If you have to take something away from this article, take this. Ignore the whole article except for this part.

Let's look the object from before:

const openApps = {
  finder: false,
  launchpad: false,
  safari: false,
  messages: false,
  mail: false,
  maps: false,
  photos: false,
  facetime: false,
  calendar: false,
};

Say I wanna apply Object.keys to get an array of the keys of this object.

const apps = Object.keys(openApps);
//  ["finder", "launchpad", "safari", "messages", "mail", "maps", "photos", "facetime", "calendar"]

But there's bit of a problem here. If you hover over apps, its type will be string[]. Not ("finder" | "launchpad" | "safari" | "messages" | "mail" | "maps" | "photos" | "facetime" | "calendar")[].

Its not exactly a problem, per se, but it would be great to have Object.keys return the union types array of the keys.

So let's investigate the issue. We'll start with Object.keys definition in pre-built lib.d.ts:

interface ObjectConstructor {
  //...
  keys(o: object): string[];
  keys(o: {}): string[];
}

If you find it weird that keys is defined twice, its called Function/Method overloading. You can basically define multiple function declarations for flexible usage.

As you can see, its hard coded to always return string[]. I'm sure its there for good reasons, but its quite inconvenient for me, so I'm gonna override this method to infer the keys correctly from what it is passed.

If you have a root .d.ts file in your project, put the snippet below right in it.

type ObjectKeys<Obj> = Obj extends object
  ? (keyof Obj)[]
  : Obj extends number
  ? []
  : Obj extends Array<any> | string
  ? string[]
  : never;

interface ObjectConstructor {
  keys<ObjectType>(o: ObjectType): ObjectKeys<ObjectType>;
}

Now let's try the code above with the new Object.keys:

const apps = Object.keys(openApps);
// const apps: ("finder" | "launchpad" | "safari" | "messages" | "mail" | "maps" | "photos" | "facetime" | "calendar")[]

Don't trust me? Check it out yourself @ TypeScript Playground

Note: All the credit goes to Steven Baumgeitner's blog post about this exact same thing. I just ripped it off ๐Ÿ˜. You can read more about fixing Object.keys on his blog post.

So, this is it!! Hope you got something out of this blog post!

Signing off!!