Dachang Technology Advanced Front-End Node Advanced Click on the top Programmer Growth Guide, pay attention to the official account Reply 1, join the advanced Node exchange group
In this article, we’ll introduce six advanced TypeScript tricks, each with examples showing how it’s implemented and used. Using these tips, you can improve not only your code quality, but also your skill level as a TypeScript programmer.
1 – Advanced Types
Using TypeScript’s advanced types, such as mapped types and conditional types, it is possible to build new types based on existing types. By using these types, you can change and manipulate types within a strongly typed system, making your code more flexible and maintainable.
Mapping type
Mapped types iterate over the properties of existing types and apply transformations to create new types. A common use case is to create a read-only version of a type.
type Readonly<T> = { readonly [P in keyof T]: T[P]; }; interface Point { x: number; y: number; } type ReadonlyPoint = Readonly<Point>;
In this example, we define a mapping type called Readonly
that takes a type T
as a generic parameter and makes all its properties read-only. We then created a ReadonlyPoint
type based on the Point
interface where all properties are read-only.
Condition Type
Conditional types allow you to create new types based on conditions. The syntax is similar to the ternary operator, using the extends keyword as a type constraint.
type NonNullable<T> = T extends null | undefined ? never : T;
In this example, we define a conditional type called NonNullable that takes a type T and checks whether it extends null or undefined. The result type is never if extended, otherwise the original type T.
Let’s extend the advanced type example to add more usability and output.
interface Point { x: number; y: number; } type ReadonlyPoint = Readonly<Point>; const regularPoint: Point = { x: 5, y: 10 }; const readonlyPoint: ReadonlyPoint = { x: 20, y: 30 }; regularPoint.x = 15; // This works as 'x' is mutable in the 'Point' interface console.log(regularPoint); // Output: { x: 15, y: 10 } // readonlyPoint.x = 25; // Error: Cannot assign to 'x' because it is a read-only property console.log(readonlyPoint); // Output: { x: 20, y: 30 } function movePoint(p: Point, dx: number, dy: number): Point { return { x: p.x + dx, y: p.y + dy }; } const movedRegularPoint = movePoint(regularPoint, 3, 4); console.log(movedRegularPoint); // Output: { x: 18, y: 14 } // const movedReadonlyPoint = movePoint(readonlyPoint, 3, 4); // Error: Argument of type 'ReadonlyPoint' is not assignable to parameter of type 'Point'
In this example, we demonstrate the use of the Readonly map type and how it enforces immutability. We create a mutable Point object and a read-only ReadonlyPoint object. We showed that attempting to modify a read-only property results in a compile-time error. We also prevent unintended side effects in your code by stating that read-only types cannot be used where mutable types are expected.
2 – Decorator
Decorators in TypeScript are a powerful feature that allow you to add metadata and modify or extend the behavior of classes, methods, properties, and parameters. They are higher-order functions that can be used to observe, modify, or replace class definitions, method definitions, accessor definitions, property definitions, or parameter definitions.
Class decorator
Class decorators are applied to the constructor of a class and can be used to modify or extend the class definition.
function LogClass(target: Function) { console.log(`Class ${target.name} was defined.`); } @LogClass class MyClass { constructor() {} }
In this example, we define a class decorator named LogClass that logs the name of the decorated class when it is defined. We then apply the decorator to the MyClass class using the @ syntax.
Method decorator
Method decorators are applied to methods of a class and can be used to modify or extend method definitions.
function LogMethod(target: any, key: string, descriptor: PropertyDescriptor) { console.log(`Method ${key} was called.`); } class MyClass { @LogMethod myMethod() { console.log("Inside myMethod."); } } const instance = new MyClass(); instance.myMethod();
In this example, we define a method decorator called LogMethod
that logs the name of the decorated method when the method is called. We then apply the decorator to the myMethod
method of the MyClass
class using the @
syntax.
Attribute decorator
Property decorators are applied to properties of a class and can be used to modify or extend property definitions.
function DefaultValue(value: any) { return (target: any, key: string) => { target[key] = value; }; } class MyClass { @DefaultValue(42) myProperty: number; } const instance = new MyClass(); console.log(instance.myProperty); // Output: 42
In this example, we define a property decorator called DefaultValue
that sets a default value for the decorated property. We then apply the decorator to the myProperty
property of the MyClass
class using the @
syntax.
parameter decorator
Parameter decorators are applied to the parameters of a method or constructor and can be used to modify or extend parameter definitions.
function LogParameter(target: any, key: string, parameterIndex: number) { console.log(`The parameter ${parameterIndex} of the method ${key} was called.`); } class MyClass { myMethod(@LogParameter value: number) { console.log(`Inside myMethod, use value ${value}.`); } } const instance = new MyClass(); instance.myMethod(5);
In this example, we define a parameter decorator named LogParameter that logs the index and name of the decorated parameter when the method is called. We then apply the decorator to the value parameter of the myMethod method of the MyClass class using the @
syntax.
3 – Namespaces
In TypeScript, namespaces are a way of organizing and grouping related code. They help you avoid naming conflicts, promote modularity by encapsulating code that belongs together. A namespace can contain classes, interfaces, functions, variables, and other namespaces.
Define namespace
To define a namespace, use the namespace
keyword followed by the namespace name. You can then add any relevant code inside the braces.
namespace MyNamespace { export class MyClass { constructor(public value: number) {} displayValue() { console.log(`The value is: ${this.value}`); } } }
In this example, we define a namespace called MyNamespace
and add a class MyClass
to it. Note that we use the export
keyword to make the class accessible outside the namespace.
Using namespaces
To use code from a namespace, you can import code using the fully qualified name or using namespace imports.
// use fully qualified name const instance1 = new MyNamespace. MyClass(5); instance1.displayValue(); // output: The value is: 5 // import using namespace import MyClass = MyNamespace. MyClass; const instance2 = new MyClass(10); instance2.displayValue(); // Output: The value is: 10
In this example, we demonstrate two ways of using the MyClass
class in the MyNamespace
namespace. First, we use the fully qualified name MyNamespace.MyClass
. Second, we import the MyClass
class using the namespace import statement and use it with a shorter name.
Nested namespace
Namespaces can be nested to create hierarchies and further organize code.
namespace OuterNamespace { export namespace InnerNamespace { export class MyClass { constructor(public value: number) {} displayValue() { console.log(`The value is: ${this.value}`); } } } } // use fully qualified name const instance = new OuterNamespace. InnerNamespace. MyClass(15); instance.displayValue(); // Output: The value is: 15
In this example, we define a nested namespace called InnerNamespace
, define a class MyClass
inside OuterNamespace
, and use The fully qualified name OuterNamespace.InnerNamespace.MyClass
uses it.
4 – Mixins
Mixins are a way of combining classes in TypeScript, consisting of multiple smaller parts called mixin classes. They allow you to reuse and share behavior between different classes, promoting modularity and code reusability.
Define mixins
To define a mixin class, create a class that extends the generic type parameter with a constructor signature. This allows mixins to be combined with other classes.
class TimestampMixin<TBase extends new (...args: any[]) => any>(Base: TBase) { constructor(...args: any[]) { super(...args); } getTimestamp() { return new Date(); } }
In this example, we define a mixin called TimestampMixin that adds a getTimestamp method that returns the current date and time. The mixin class extends TBase with a generic type parameter with a constructor signature to allow it to be composed with other classes.
Using mixins
To use a mixin class, define a base class and apply the mixin class to it using the extends
keyword.
class MyBaseClass { constructor(public value: number) {} displayValue() { console.log(`The value is: ${this.value}`); } } class MyMixedClass extends TimestampMixin(MyBaseClass) { constructor(value: number) { super(value); } }
In this example, we define a base class named MyBaseClass that contains a displayValue method. We then create a new class called MyMixedClass that extends the base class and applies the TimestampMixin mixin to it.
Let’s demonstrate how mixin classes work in practice.
const instance = new MyMixedClass(42); instance.displayValue(); // Output: The value is: 42 const timestamp = instance. getTimestamp(); console.log(`The timestamp is: ${timestamp}`); // output: The timestamp is: [current date and time]
In this example, we create an instance of the MyMixedClass class that includes the displayValue method of MyBaseClass and the getTimestamp method of the TimestampMixin mixin class. Then, we call these two methods and display their output.
5 – type protection
Type guards in TypeScript are a way of narrowing the scope of the type of a variable or parameter within a particular block of code. They allow you to distinguish between different types, and to access properties or methods specific to those types, promoting type safety and reducing the possibility of runtime errors.
Define type protection
To define a type guard, create a function that takes a variable or parameter and returns a type predicate. A type predicate is a Boolean expression that narrows the type of an argument within the scope of a function.
function isString(value: any): value is string { return typeof value === "string"; }
In this example, we define a type guard function isString
which checks if a given value is of type string
. The function returns a type predicate value is string
that narrows the type of the value
parameter within the scope of the function.
Using type protection
To use type guards, simply call the type guard function within a conditional statement such as an if
statement or a switch
statement.
function processValue(value: string | number) { if (isString(value)) { console.log(`The length of the string is: ${value.length}`); } else { console.log(`The square of the number is: ${value * value}`); } }
In this example, we define a function called processValue
that takes a value of type string | number
. We use the isString
type guard function to check if a value is a string. If it’s a string, we access the length
property specific to the string type. Otherwise, we assume the value is a number and calculate its square.
Let’s demonstrate how type guards work in practice.
processValue("hello"); // output: The length of the string is: 5 processValue(42); // Output: The square of the number is: 1764
In this example, we call the processValue
function and pass in a string and a number. The type guard function isString
ensures that the appropriate block of code is executed for each type, allowing us to access type-specific properties and methods without raising any type errors.
6 – Utility Types
Utility types in TypeScript provide a convenient way to convert existing types into new types. They allow you to create more complex and flexible types without defining them from scratch, promoting code reusability and type safety.
Using utility types
To use a utility type, use the angle bracket syntax to apply the utility type to an existing type. TypeScript provides various built-in utility types such as Partial
, Readonly
, Pick
, and Omit
.
interface Person { name: string; age: number; email: string; } type PartialPerson = Partial<Person>; type ReadonlyPerson = Readonly<Person>; type NameAndAge = Pick<Person, "name" | "age">; type WithoutEmail = Omit<Person, "email">;
In this example, we define an interface called Person
with three properties: name
, age
and email
. We then created new types based on the Person
interface using various built-in utility types.
Let’s demonstrate how these utility types actually work.
Partial
const partialPerson: PartialPerson = { name: "John Doe", };
In this example, we create a partialPerson
object of type PartialPerson
. The Partial
utility type makes all properties of the Person
interface optional, allowing us to create partial persons with only the name
property.
Readonly
const readonlyPerson: ReadonlyPerson = { name: "Jane Doe", age: 30, email: "[email protected]", }; // readonlyPerson.age = 31; // Error: Cannot assign to 'age' because it is a read-only property.
In this example, we create a readonlyPerson
object of type ReadonlyPerson
. The Readonly
utility type makes all properties of the Person
interface read-only, preventing us from modifying the age
property.
Pick
const nameAndAge: NameAndAge = { name: "John Smith", age: 25, }; // nameAndAge.email; // Error: Property 'email' does not exist on type 'Pick<Person, "name" | "age">'.
In this example, we create a nameAndAge
object of type NameAndAge
. The Pick
utility type creates a new type that contains only the specified properties of the Person
interface, in this case name
and age
.
Omit
const withoutEmail: WithoutEmail = { name: "Jane Smith", age: 28, }; // withoutEmail.email; // Error: Property 'email' does not exist on type 'Omit<Person, "email">'.
In this example, we create a withoutEmail
object of type WithoutEmail
. Omit
uses the Person interface to create a new type from which the specified property is removed, here the email property. This allows us to use the withoutEmail object to represent a Person object without an email property.
const withoutEmail: WithoutEmail = { name: "Jane Smith", age: 28, }; // withoutEmail.email; // Error: Property 'email' does not exist on type 'Omit<Person, "email">'
In the above example, we created a withoutEmail object of type WithoutEmail. Omit uses the Person interface to create a new type from which the specified property is removed, here the email property. This allows us to use the withoutEmail object to represent a Person object without an email property.
Summary
In summary, this article explored various advanced TypeScript topics such as namespaces, advanced types, decorators, mixins, type guards, and utility types. By understanding and leveraging these features, you can create more modular, reusable, and maintainable code that conforms to best practices and reduces the likelihood of runtime errors.
By taking advantage of these advanced TypeScript features, you can write cleaner, more organized, and more maintainable code, taking full advantage of TypeScript’s powerful type system and language features.
If you like this article and find it helpful, you can like it and forward it, so that more people can write code clearly!
About this article
Author: simple_lau
https://juejin.cn/post/7225534193995104312
Node community
I have formed a Node.js community with a particularly good atmosphere, and there are many Node.js friends in it. If you are interested in learning Node.js (following plans are also possible), we can conduct Node.js-related activities together. Exchange, learn and build together. Just add a koala friend below and reply “Node”.
"Share, like, watch" support a wave