Unveiling TypeScript Hidden Gems: Beyond the Basics
TypeScript has gained immense popularity for bringing static typing to JavaScript, making it a powerful tool for building scalable and maintainable applications. TypeScript, being a superset of JavaScript, extends its capabilities by introducing powerful features that might not be as well-known.
It’s expected that readers of this blog have a sound understanding of the ins and outs of TypeScript. Please also consider giving the docs on Utility Types and Types from Types in the TypeScript handbook a read if you want to learn more.
In this advanced exploration, we’ll uncover some hidden tips, tricks, and gems that go beyond the commonly used TypeScript features. Let’s go!
1. Mapped Types
Mapped types are powerful tools for transforming existing types into new ones. Combining them with keyof
operator allows you to create generic and reusable utility types. For example, you can create a utility that removes the readonly
modifier from all properties of an existing type.:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type User = {
readonly name: string;
readonly age: number;
};
type MutableUser = Mutable<User>;
// Result (readonly removed): { name: string; age: number; }
Fun Fact: TypeScript’s
Partial
,Required
, andReadonly
utility types are based on this feature.
This technique can be handy for scenarios where you need variations of existing types, thereby providing flexibility in defining object types. This helps in cases like manipulating object keys, allowing for the extraction, transformation, or restriction of specific properties within an object type.
2. Template Literal Types
One fascinating feature in TypeScript is the ability to specify the structure of a string using Template Literal Types. This allows you to create intricate patterns for strings, ensuring a higher level of type safety. Let’s look at an example:
type Domains = 'com' | 'edu' | 'co.in';
type Providers = 'gmail' | 'yahoo' | 'protonmail';
type Email = `${string}@${Providers}.${Domains}`;
let userEmail: Email = 'user@example.com'; // Type-checked email structure
let invalidEmail: Email= 'invalid@yahoo.net'; // Compile-time error
This way, you can define and enforce specific patterns for strings, such as emails, and addresses, making your code more robust. This supports creating formatted string literal types, allowing for the definition of specific patterns for structured data beyond just strings.
3. Conditional Types
Conditional Types provide a way to perform type transformations based on conditions. You can create sophisticated logic within type definitions. Here’s an example of a conditional type that extracts the type of numeric properties from an object:
type NumericProperties<T> = {
[K in keyof T]: T[K] extends number ? K : never;
};
interface Example {
name: string;
age: number;
score: number;
}
type NumericKeys = NumericProperties<Example>[keyof Example];
// Result: "age" | "score"
Fun Fact: TypeScript’s
Exclude
andExtract
utility types are based on this feature.
This can also be used for inferring and narrowing types based on the outcome of conditionals, expanding their utility beyond type transformation.
4. Inferred Conditional Types
The infer
keyword in TypeScript is a subtle yet powerful tool that complements Conditional types. It allows you to introduce a type variable within a conditional type, making your types more dynamic. Let's see it in action:
type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getStringLength(str: string): number {
return str.length;
}
type StringLength = ExtractReturnType<typeof getStringLength>;
// StringLength is now `number`
Fun Fact: TypeScript’s
Parameters
,ReturnType
andInstanceType
utility types are based on this feature.
This can be particularly useful when working with utility functions or generic types where you want to extract specific information.
5. Inferred Intrinsic Types
TypeScript also supports intrinsic string manipulation types, allowing you to perform operations directly on string literals. These types enhance the expressiveness of your code, especially when dealing with string transformations.
Available Intrinsic types include:
Uppercase
,Lowercase
,Capitalize
, and,Uncapitalize
.
While these Intrinsic types use the JavaScript string runtime functions directly for manipulation, we however can build our own custom Intrinsic types out of the box by leveraging the power of infer
. As an example:
type TitleCase<S extends string = string> =
S extends `${infer FirstLetter}${infer Rest}`
?
Rest extends `${infer Word} ${infer Chars}`
? `${Uppercase<FirstLetter>}${Lowercase<Word>} ${TitleCase<Chars>}`
: `${Uppercase<FirstLetter>}${Lowercase<Rest>}`
: '';
type TitleCased = TitleCase<"hello Hello HELLO heLlO">;
// TitleCased = "Hello Hello Hello Hello"
6. Distributive Conditional Types
Understanding distributive conditional types can lead to more flexible type definitions. When a conditional type is instantiated with a union type, it distributes the operation over each member of the union. Here’s a simple example:
type Box<T> = { value: T };
type Boxify<T> = T extends any ? Box<T> : never;
type StringBox = Boxify<string | number>;
// Result: Box<string> | Box<number>
This allows you to create utility types that operate on each element of a union type.
7. Recursive Types
Creating recursive types can be useful for scenarios where you need nested structures. TypeScript allows you to express recursive types through the type
keyword. Here's an example of a simple recursive list:
type RecursiveList<T> = {
value: T;
next?: RecursiveList<T>;
};
const myList: RecursiveList<number> = {
value: 1,
next: {
value: 2,
next: {
value: 3,
},
},
};
This technique is not limited to simple linked lists but can be utilized to model complex recursive structures, such as trees and graphs, providing robust support for various recursive data structures and algorithms.
8. Inferred Recursive Conditional Types
Recursive conditional types enable the creation of dynamic and recursive type structures. The infer
keyword plays a crucial role in achieving this flexibility within conditional types. Let's delve into inferred recursive conditional types with an example:
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
type NestedArray = [1, [2, [3, [4]]]];
type FlatArray = Flatten<NestedArray>;
// Type: 1 | 2 | 3 | 4
Fun Fact: TypeScript’s
Awaited
utility type is based on this feature.
This inferred recursive conditional type pattern is powerful for scenarios where you need to handle complex nested structures, providing a concise and expressive way to define types in TypeScript.
These hidden gems in TypeScript showcase the language’s depth and versatility. By leveraging these features, you can enhance the expressiveness and robustness of your TypeScript code. Experimenting with these features will not only expand your understanding but also empower you to write more sophisticated and type-safe code.
Happy “typing”! 🖥️