· engineering · 9 min read
Advanced TypeScript features you may not know about
Improve your TypeScript code with these must-have features
TypeScript has become increasingly popular as a tool for web development due to its ability to enhance JavaScript with static type checking and other advanced features. These features can help developers write more maintainable and error-free code, leading to better application reliability.
While many developers are already familiar with the basics of TypeScript, there are a variety of advanced features that can provide even more benefits. These include things like generics, template literal types, type guards and infer keyword, which can be used to create more precise and flexible code.
This article will delve into these advanced TypeScript features and explain how they can be utilized to solve common problems and improve web application robustness. Whether you’re a beginner or a seasoned developer looking to expand your knowledge, this guide will provide valuable insights into the powerful capabilities of TypeScript.
Jump ahead:
1. Generics
Generics offer a means of creating code that is adaptable and capable of working with multiple data types, rather than being restricted to a single type. This enables users of the code to specify their own types, providing greater flexibility and reusability.
To define a generic type in TypeScript, you can use angle brackets (<>) and a placeholder name for the type, which can then be used as a type annotation in function or class declarations. For example, you might define a function that takes an array of a certain type and returns a new array with that type:
function reverse<T>(arr: T[]): T[] {
return arr.reverse();
}
In this example, the <T>
syntax defines a generic type placeholder that can be used to represent any type of array. The function then takes an array of this generic type and returns a new array with the same type.
This can be useful for creating more reusable and type-safe code, as the function can be called with different types of arrays without having to redefine the function for each type.
Generics can also be used with classes to create reusable and type-safe data structures. For example, you might define a generic class for a queue data structure:
class Queue<T> {
private items: T[] = [];
enqueue(item: T) {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
In this example, the <T>
syntax is used to define a generic type placeholder for the items in the queue. This allows the class to be used with different types of items, while still providing type safety and avoiding the need for duplicated code.
Overall, generics are a powerful feature of TypeScript that can greatly improve the flexibility and maintainability of your code. By defining generic types that can be used across different parts of your code, you can create more reusable and type-safe functions and classes that are easier to maintain and extend.
2. Template Literal Types
Template Literal Types are a powerful feature in TypeScript that allow you to create complex types by combining literal strings and expressions in a template-like format. Introduced in version 4.1, they provide a flexible way to define types that are more precise and reusable.
To define a Template Literal Type, you use backticks (`) to enclose a string template that can include placeholders for expressions.
type NumberAndString = `${number}-${string}`;
Let’s say you have a set of heading and paragraph tags, and you want to create a type that represents all possible combinations of these tags with a “tag” suffix. You could define the heading and paragraph tags as string literals:
type Headings = "h1" | "h2" | "h3" | "h4" | "h5";
type Paragraphs = "p";
Then, you can use a Template Literal Type to concatenate these literals with the “tag” suffix:
type AllLocaleIDs = `${Headings | Paragraphs}_tag`;
In this example, the AllLocaleIDs
type is defined using a string template that combines the Headings
and Paragraphs
literals with the “_tag” suffix to create all possible tag combinations such as "h1_tag"
, "h2_tag"
, "p_tag"
, and so on.
You can use this type to create reusable and precise types for various scenarios, such as in defining CSS class names:
function addClass(className: AllLocaleIDs) {
// ...
}
In this example, the AllLocaleIDs
type is used to ensure that only valid tag names with the “tag” suffix are accepted as the className
argument.
Template Literal Types provide a flexible way to define more precise and reusable types in TypeScript, especially when used in combination with other type features like union and intersection types.
3. Type Guards
Type Guards are a TypeScript feature that allow you to check the type of a variable at runtime and perform different actions depending on the type. In other words, they provide a way to narrow down the type of a variable within a conditional statement. This is useful when working with variables that could have more than one possible type.
TypeScript provides some built-in JavaScript operators that can be used as type guards, including the typeof
, instanceof
, and in
operators. Type guards can be used to detect the correct methods, prototypes, and properties of a value, similar to feature detection. They help ensure that the type of an argument is what you say it is, by allowing you to instruct the TypeScript compiler to infer a specific type for a variable in a particular context.
There are five major ways to use a type guard:
- The
instanceof
keyword - The
typeof
keyword - The
in
keyword - Equality narrowing type guard
- Custom type guard with predicate
In this article, we will cover the first three methods.
The instanceof
type guard checks if a value is an instance of a given constructor function or class. With this type guard, we can test if an object or value is derived from a class, which is useful for determining the type of an instance. The syntax for the instanceof
type guard is:
objectVariable instanceof ClassName;
In the following example, we use the instanceof
type guard to determine the type of an Accessory
object:
interface Accessory {
brand: string;
}
class Necklace implements Accessory {
kind: string;
brand: string;
constructor(brand: string, kind: string) {
this.brand = brand;
this.kind = kind;
}
}
class Bracelet implements Accessory {
brand: string;
year: number;
constructor(brand: string, year: number) {
this.brand = brand;
this.year = year;
}
}
const getRandomAccessory = () => {
return Math.random() < 0.5 ?
new Bracelet('Cartier', 2021) :
new Necklace('Choker', 'TASAKI');
}
let accessory = getRandomAccessory();
if (accessory instanceof Bracelet) {
console.log(accessory.year);
}
if (accessory instanceof Necklace) {
console.log(accessory.brand);
}
The typeof
type guard is used to determine the type of a variable. The typeof
operator is said to be limited and shallow because it can only determine certain types recognized by JavaScript, such as boolean
, string
, bigint
, symbol
, undefined
, function
, and number
. For anything outside of this list, the typeof
type guard simply returns object
. The syntax for the typeof
type guard is:
typeof v !== "typename"
// or
typeof v === "typename"
In the following example, we use the typeof
type guard to determine the type of a variable x
:
function StudentId(x: string | number) {
if (typeof x == 'string') {
console.log('Student');
}
if (typeof x === 'number') {
console.log('ID');
}
}
StudentId(`446`); // prints 'Student'
StudentId(446); // prints 'ID'
The in
type guard checks if an object has a particular property, using that to differentiate between different types. It returns a boolean that indicates if the property exists in that object. The syntax for the in
type guard is:
propertyName in objectName
Another similar example of how the in
type guard works is shown below:
interface Cat {
name: string;
purr(): void;
}
interface Dog {
name: string;
bark(): void;
}
function isCatOrDog(pet: Cat | Dog): pet is Cat | Dog {
return 'purr' in pet;
}
function petSounds(pet: Cat | Dog) {
if (isCatOrDog(pet)) {
if ('purr' in pet) {
pet.purr();
} else {
pet.bark();
}
}
}
TypeScript type guards are helpful for assuring the value of a type, improving the overall code flow. Most of the time, your use case can be solved using either the instanceof
type guard, the typeof
type guard, or the in
type guard, however, you can use a custom type guard when it is absolutely necessary.
4. Infer Keyword
infer
is a keyword in TypeScript used in conditional types to infer a type from another type. It allows you to extract and use a type from a given type, which can be useful for building generic types that work with different data structures.
For example, we can declare a new type variable “R” in a type “MyType
type MyType<T> = T extends infer R ? R : never;
type T1 = MyType<{b: string}> // T1 is { b: string; }
Here, T1
is inferred to be ”{ b: string; }” because the type ”{ b: string; }” is assignable to R
.
If we try to use an undeclared type parameter without using “infer”, the compiler will throw a compile error:
type MyType2<T> = T extends R2 ? R2 : never; // error, R2 undeclared
On the other hand, if we omit infer
and compare T
directly to R
the compiler checks if T
is assignable to R
:
type R = { a: number }
type MyType3<T> = T extends R ? R : never;
type T3 = MyType3<{b: string}> // T3 is never
In this case, T3
is never
because ”{ b: string; }” is not assignable to ”{ a: number; }“.
Finally, note that infer R
shadows type references of an equally-named type declaration R
:
type R = { a: number }
type MyType4<T> = T extends infer R ? R : never;
type T4 = MyType4<{b: string}> // T4 is { b: string; }
Here, T4
is inferred to be ”{ b: string; }” because the type variable R
declared in MyType4
shadows the type reference of the equally-named type declaration R
.
Conclusion
In conclusion, TypeScript is a powerful and flexible language that offers many advanced features beyond basic type annotations. By leveraging these features, developers can write safer, more maintainable code with fewer errors and better readability. Some of the features discussed in this article include generics, template literal types, type guards and the infer keyword.
By mastering these features and incorporating them into your development workflow, you can take full advantage of TypeScript’s capabilities and write more efficient, robust code.
I hope you enjoyed this article!
Subscribe to my newsletter to get the latest updates on my blog.