Mastering TypeScript - 6 Advanced Tips and Tricks

Mastering TypeScript - 6 Advanced Tips and Tricks

Sunny Sun Lv4

** This post is update on 27-Jul-2024

In this article:

TypeScript is transforming the way we write JavaScript, offering new features that make code more concise, powerful, and efficient with every release. However, the rapid pace of innovation can leave developers struggling to keep up.

This article will walk through 6 lesser-known yet incredibly useful TypeScript features and explore how to use them to enhance your TypeScript development experience.

Key Remapping with Mapped Types

Key remapping in mapped types is a powerful feature that allows renaming keys in an object type.

To perform key remapping, We need to use the as keyword followed by a type expression that describes the new key. Below is the basic syntax:

1
2
3
type MappedTypeWithNewProperties<T> = {  
[Properties in keyof T as NewKeyType]: Type[Properties]
}

One of the common use cases of key remapping is to create a new type with renamed keys.

1
2
3
4
5
6
7
8
9
10
11
type Person = {  
name: string;
age: number;
};
type NewPerson = {
[K in keyof Person as `new_${string & K}`]: Person[K];
};
const John : NewPerson = {
new_name: 'John',
new_age: 10
}

In the above example, the NewPerson type uses a mapped type to rename the keys in the Person type. The K variable represents the keys in the Person type, and the new_${string & K} syntax is used to rename the keys. This can be useful when you want to derive a new type based on an existing Type.

You can also use this feature to add or remove keys, making creating a well-constrained type system for your App more flexible.

Short-Circuiting Assignment Operators

Short-circuiting assignment operators are introduced in TypeScript 4.0. It allows you to perform multiple operations in a single line of code. Consider the following example.

1
2
3
4
5
6
7
let x: number | undefined;  
// Without short-circuiting assignment
if (!x) {
x= 42;
}
// With short-circuiting assignment
x ||= 42; // x is 42 if null

Here, the “||=” operator assigns the value 42 to “x” if it’s null or undefined. This can be a useful shorthand for writing if statements and can help reduce the amount of code you need to write.

Three operators are supported, including logical and (&&), logical or (||), and nullish coalescing (??). Below is another example:

1
2
3
4
5
6
7
8
9
let scores: number[] | undefined;  

// Without short-circuiting assignment
if(!!scores){
scores = [];
scores.push(99);
}
// With short-circuiting assignment
(scores??= []).push(99);

As shown in the example, using the new short-circuiting assignment operator make the code more concise.

Labeled Tuple Elements

In TypeScript, Tuple is a data structure that allows us to store a fixed-size sequence of elements of different types. The value of a tuple can be accessed via array-style indexing:

1
2
let john: [string, number] = ["John", 20];  
let age_of_John: number= john[1];

As shown above, using indexing is hard to express the intent. We also need to ensure the type of the element is correct. Otherwise, an error will be thrown.

Labeled tuple elements are a feature introduced in TypeScript 4.0. It allows you to label the elements of a tuple. Below is an example:

1
2
3
4
type Person= [name: string, age: number];  
const myTuple: Person = ['John', 30];
console.log(myTuple.name); // 'John'
console.log(myTuple.age); // 30

Here, the elements of the MyTuple type are labeled as name and age, makes it more readable. The labels can also be used to access the tuple’s elements, which can be especially useful when working with complex data structures.

Use Index Signatures to define an Object with an unknown structure

Index signatures in TypeScript provide a way to define an object’s properties with a dynamic key. This is useful when we know the type of the properties, but the names of the properties are unknown. To use index signatures, we need to declare the key type as below.

1
{ [key: KeyType]: ValueType }

Below is an example:

1
2
3
4
5
6
7
8
9
interface MyObj {  
[key: string]: number;
}

const obj: MyObj = {
foo: 1,
bar: 2,
baz: 3
};

Here, we defined an interface MyObj with an index signature [key: string]: number. This means that MyObj can have any number of properties with string keys and number values. We then created an object obj that conforms to the MyObj interface, with properties foo, bar, and baz.

The key of the index signature can only be a string, number, symbol or template literal type. If you try to use other types, an error will be thrown.

Error when trying to use number array to key

Note that you can also use index signatures with static properties.

1
2
3
4
5
class MyStaticClass {  
static [key: string]: number;
}

MyStaticClass.foo = 1;

The static index signature is only available for TypeScript 4.3 and above.

Use destructuring to unpack values

Destructuring is a powerful feature that allows us to extract values from arrays or objects into distinct variables.

Here is a basic example:

1
2
3
4
5
6
7
8
9
10
11
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
// from object properties
const {firstName, lastName, age} = user;

// array destructuring
const count= ['one', 'two', 'three'];
const [first, second, third] = count;

Another useful destructing features are provided, including default value.

1
2
3
const user = { firstName: 'John' };
const { firstName, lastName = 'Doe' } = user;
console.log(lastName); // Output: Doe

Destructuring can also be nested for complex objects:

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'Anytown'
}
};

const { name, address: { street } } = person;
console.log(name, street); // Output: John Doe 123 Main St

using destructuring syntax enhances code readability and maintainability by reducing verbosity and improving data accessibility.

Use satisfies operator to validate against another type

satisfiesis a new feature that was introduced in TypeScript 4.9. It allows developers to define a type that must satisfy certain types but not affect the original type. This will be useful when you want to ensure that a type conforms to a specific set of rules and preserve the original type simultaneously.

Let’s say we have a config object containing name and count properties.

1
2
3
4
5
6
7
8
9
10
const config = {  
name: 'My App',
count: 5
}

type ConfigType = Record<'name'|'count', number | string>;
const config: ConfigType = {
name: 'My App',
count: 5
}

we can use a ConfigType to apply type constraint to it so that we can catch any typo in compile time.

But there is a side effect of this approach; we lost the original type inference. The count property was a number type, and now it has become a union of string | number.

The satisfies operator provides a better solution.

1
2
3
4
const config2 = {  
name: 'My App'
count: 10,
} satisfies ConfigType;

Here we use satisfies to validate the object with ConfigType, and simultaneously, the original type inference is preserved.

Summary

TypeScript offers a wealth of features to enhance code quality and developer productivity. While many developers are familiar with its core concepts, there are hidden gems that can significantly elevate your TypeScript coding experience. This article explores six powerful TypeScript features that can help you write cleaner, more maintainable, and expressive code. I hope you find the article useful.

  • Title: Mastering TypeScript - 6 Advanced Tips and Tricks
  • Author: Sunny Sun
  • Created at : 2023-08-01 00:00:00
  • Updated at : 2024-07-27 15:00:45
  • Link: http://coffeethinkcode.com/2023/08/01/unlocking-typescript-5-hidden-gems/
  • License: This work is licensed under CC BY-NC-SA 4.0.