DEV Community

Cover image for Mindblowing 🤯 TypeScript tricks
PuruVJ
PuruVJ

Posted on • Originally published at puruvj.dev

Mindblowing 🤯 TypeScript tricks

Read on my blog in light, dark or sepia mode

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';
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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'),
};
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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[]'.
Enter fullscreen mode Exit fullscreen mode

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

type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType[number];
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

usage:

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

const num: UnwrapPromise<typeof returnsPromise> = 8;
//    num: number
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

And here's the universal type safe helper:

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

Usage:

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

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

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,
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Usage👇

type App = KeysOfObject<typeof openApps>;
Enter fullscreen mode Exit fullscreen mode

This will be equal to 👇

type App =
  | 'finder'
  | 'launchpad'
  | 'safari'
  | 'messages'
  | 'mail'
  | 'maps'
  | 'photos'
  | 'facetime'
  | 'calendar';
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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[];
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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")[]
Enter fullscreen mode Exit fullscreen mode

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!!

Top comments (4)

Collapse
 
dgreene1 profile image
Dan Greene • Edited

It might be advisable to share that there’s a reason why Object.keys is always an array of strings, and that’s that the Typescript team feels that you have no way of knowing that an object will have exactly the keys that are on the interface. In reality, it’s very common for objects to have more properties at runtime than they due at compile time because of Typescript’s structural subtyping.

So in a sense, your Object keys alternative implies that there will be a definitive set of string literals in the union, but technically it could be an infinite number (hence the Typescript team just calling it a string).

Collapse
 
puruvj profile image
PuruVJ

Good point. I didn't know myself 😅

Collapse
 
tbhaxor profile image
Gurkirat Singh

Just to let everyone know readonly != immutable

I had this discussion with typescript team here

So for only a particular type readonly has meaning

Collapse
 
puruvj profile image
PuruVJ

Good point Gurkirat! 👍