Get to know TypeScript series:
Part 1 - An Ode to TypeScript
Part 2 - Using TypeScript without TypeScript
Part 3 - React TypeScript Hooks issue when returning array
Part 4 - Mindblowing TypeScript tricks (You're reading it )
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 ofPromise
?
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 thatPromise
s do. In that case, we usePromiseLike
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 asstring[]
, 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 wordtuple
. The reason is that tuple isn't a keyword. As far as TypeScript is concerned, areadonly Array
is a tuple. There's noTuple
type or anything. That's why I'm making sure the type passed toUnionFromTuple
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!!
Get to know TypeScript series:
Part 1 - An Ode to TypeScript
Part 2 - Using TypeScript without TypeScript
Part 3 - React TypeScript Hooks issue when returning array
Part 4 - Mindblowing TypeScript tricks (You're reading it )