One place for hosting & domains

      TypeScript

      How To Use Generics in TypeScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Generics are a fundamental feature of statically-typed languages, allowing developers to pass types as parameters to another type, function, or other structure. When a developer makes their component a generic component, they give that component the ability to accept and enforce typing that is passed in when the component is used, which improves code flexibility, makes components reusable, and removes duplication.

      TypeScript fully supports generics as a way to introduce type-safety into components that accept arguments and return values whose type will be indeterminate until they are consumed later in your code. In this tutorial, you will try out real-world examples of TypeScript generics and explore how they are used in functions, types, classes, and interfaces. You will also use generics to create mapped types and conditional types, which will help you create TypeScript components that have the flexibility to apply to all necessary situations in your code.

      Prerequisites

      To follow this tutorial, you will need:

      • An environment in which you can execute TypeScript programs to follow along with the examples. To set this up on your local machine, you will need the following:
      • If you do not wish to create a TypeScript environment on your local machine, you can use the official TypeScript Playground to follow along.
      • You will need sufficient knowledge of JavaScript, especially ES6+ syntax, such as destructuring, rest operators, and imports/exports. If you need more information on these topics, reading our How To Code in JavaScript series is recommended.
      • This tutorial will reference aspects of text editors that support TypeScript and show in-line errors. This is not necessary to use TypeScript, but does take more advantage of TypeScript features. To gain the benefit of these, you can use a text editor like Visual Studio Code, which has full support for TypeScript out of the box. You can also try out these benefits in the TypeScript Playground.

      All examples shown in this tutorial were created using TypeScript version 4.2.3.

      Generics Syntax

      Before getting into the application of generics, this tutorial will first go through syntax for TypeScript generics, followed by an example to illustrate their general purpose.

      Generics appear in TypeScript code inside angle brackets, in the format <T>, where T represents a passed-in type. <T> can be read as a generic of type T. In this case, T will operate in the same way that parameters work in functions, as placeholders for a type that will be declared when an instance of the structure is created. The generic types specified inside angle brackets are therefore also known as generic type parameters or just type parameters. Multiple generic types can also appear in a single definition, like <T, K, A>.

      Note: By convention, programmers usually use a single letter to name a generic type. This is not a syntax rule, and you can name generics like any other type in TypeScript, but this convention helps to immediately convey to those reading your code that a generic type does not require a specific type.

      Generics can appear in functions, types, classes, and interfaces. Each of these structures will be covered later in this tutorial, but for now a function will be used as an example to illustrate the basic syntax of generics.

      To see how useful generics are, imagine that you have a JavaScript function that takes two parameters: an object and an array of keys. The function will return a new object based on the original one, but with only the keys you want:

      function pickObjectKeys(obj, keys) {
        let result = {}
        for (const key of keys) {
          if (key in obj) {
            result[key] = obj[key]
          }
        }
        return result
      }
      

      This snippet shows the pickObjectKeys() function, which iterates over the keys array and creates a new object with the keys specified in the array.

      Here is an example showing how to use the function:

      const language = {
        name: "TypeScript",
        age: 8,
        extensions: ['ts', 'tsx']
      }
      
      const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])
      

      This declares an object language, then isolates the age and extensions property with the pickObjectKeys() function. The value of ageAndExtensions would be as follows:

      {
        age: 8,
        extensions: ['ts', 'tsx']
      }
      

      If you were to migrate this code to TypeScript to make it type-safe, you would have to use generics. You could refactor the code by adding the following highlighted lines:

      function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {
        let result = {} as Pick<T, K>
        for (const key of keys) {
          if (key in obj) {
            result[key] = obj[key]
          }
        }
        return result
      }
      
      const language = {
        name: "TypeScript",
        age: 8,
        extensions: ['ts', 'tsx']
      }
      
      const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])
      

      <T, K extends keyof T> declares two parameter types for the function, where K is assigned a type that is the union of the keys in T. The obj function parameter is then set to whatever type T represents, and keys to an array of whatever type K represents. Since T in the case of the language object sets age as a number and extensions as an array of strings, the variable ageAndExtensions will now be assigned the type of an object with properties age: number and extensions: string[].

      This enforces a return type based on the arguments supplied to pickObjectKeys, allowing the function the flexibility to enforce a typing structure before it knows the specific type it needs to enforce. This also adds a better developer experience when using the function in an IDE like Visual Studio Code, which will create suggestions for the keys parameter based on the object you provided. This is shown in the following screenshot:

      TypeScript suggestion based on the type of the object

      With an idea of how generics are created in TypeScript, you can now move on to exploring using generics in specific situations. This tutorial will first cover how generics can be used in functions.

      Using Generics with Functions

      One of the most common scenarios for using generics with functions is when you have some code that is not easily typed for all use cases. In order to make the function apply to more situations, you can include generic typing. In this step, you will run through an example of an identity function to illustrate this. You will also explore an asynchronous example of when to pass type parameters into your generic directly, and how to create constraints and default values for your generic type parameters.

      Assigning Generic Parameters

      Take a look at the following function, which returns what was passed in as the first argument:

      function identity(value) {
        return value;
      }
      

      You could add the following code to make the function type-safe in TypeScript:

      function identity<T>(value: T): T {
        return value;
      }
      

      You turned your function into a generic function that accepts the generic type parameter T, which is the type of the first argument, then set the return type to be the same with : T.

      Next, add the following code to try out the function:

      function identity<T>(value: T): T {
        return value;
      }
      
      const result = identity(123);
      

      result has the type 123, which is the exact number that you passed in. TypeScript here is inferring the generic type from the calling code itself. This way the calling code does not need to pass any type parameters. You can also be explicit and set the generic type parameters to the type you want:

      function identity<T>(value: T): T {
        return value;
      }
      
      const result = identity<number>(123);
      

      In this code, result has the type number. By passing in the type with the <number> code, you are explicitly letting TypeScript know that you want the generic type parameter T of the identity function to be of type number. This will enforce the number type as the argument and the return value.

      Passing Type Parameters Directly

      Passing type parameters directly is also useful when using custom types. For example, take a look at the following code:

      type ProgrammingLanguage = {
        name: string;
      };
      
      function identity<T>(value: T): T {
        return value;
      }
      
      const result = identity<ProgrammingLanguage>({ name: "TypeScript" });
      

      In this code, result has the custom type ProgrammingLanguage because it is passed in directly to the identity function. If you did not include the type parameter explicitly, result would have the type { name: string } instead.

      Another example that is common when working with JavaScript is using a wrapper function to retrieve data from an API:

      async function fetchApi(path: string) {
        const response = await fetch(`https://example.com/api${path}`)
        return response.json();
      }
      

      This asynchronous function takes a URL path as an argument, uses the fetch API to make a request to the URL, then returns a JSON response value. In this case, the return type of the fetchApi function is going to be Promise<any>, which is the return type of the json() call on the fetch’s response object.

      Having any as a return type is not very helpful. any means any JavaScript value, and by using it you are losing static type-checking, one of the main benefits of TypeScript. If you know that the API is going to return an object in a given shape, you can make this function type-safe by using generics:

      async function fetchApi<ResultType>(path: string): Promise<ResultType> {
        const response = await fetch(`https://example.com/api${path}`);
        return response.json();
      }
      

      The highlighted code turns your function into a generic function that accepts the ResultType generic type parameter. This generic type is used in the return type of your function: Promise<ResultType>.

      Note: As your function is async, you must return a Promise object. The TypeScript Promise type is itself a generic type that accepts the type of the value the promise resolves to.

      If you take a closer look at your function, you will see that the generic is not being used in the argument list or any other place that TypeScript would be able to infer its value. This means that the calling code must explicitly pass a type for this generic when calling your function.

      Here is a possible implementation of the fetchApi generic function to retrieve user data:

      type User = {
        name: string;
      }
      
      async function fetchApi<ResultType>(path: string): Promise<ResultType> {
        const response = await fetch(`https://example.com/api${path}`);
        return response.json();
      }
      
      const data = await fetchApi<User[]>('/users')
      
      export {}
      

      In this code, you are creating a new type called User and using an array of that type (User[]) as the type for the ResultType generic parameter. The data variable now has the type User[] instead of any.

      Note: As you are using await to asynchronously process the result of your function, the return type is going to be the type of T in Promise<T>, which in this case is the generic type ResultType.

      Default Type Parameters

      Creating your generic fetchApi function like you are doing, the calling code always has to provide the type parameter. If the calling code does not include the generic type, ResultType would be bound to unknown. Take for example the following implementation:

      async function fetchApi<ResultType>(path: string): Promise<ResultType> {
        const response = await fetch(`https://example.com/api${path}`);
        return 
      response.json();
      }
      
      const data = await fetchApi('/users')
      
      console.log(data.a)
      
      export {}
      

      This code tries to access a theoretical a property of data. But since the type of data is unknown, this code will not be able to access a property of the object.

      If you are not planning to add a specific type to every single call of your generic function, you can add a default type to the generic type parameter. This can be done by adding = DefaultType right after the generic type, like this:

      async function fetchApi<ResultType = Record<string, any>>(path: string): Promise<ResultType> {
        const response = await fetch(`https://example.com/api${path}`);
        return response.json();
      }
      
      const data = await fetchApi('/users')
      
      console.log(data.a)
      
      export {}
      

      With this code, there is no longer a need for you to pass a type to the ResultType generic parameter when calling the fetchApi function, as it has a default type of Record<string, any>. This means TypeScript will recognize data as an object with keys of type string and values of type any, allowing you to access its properties.

      Type Parameters Constraints

      In some situations, a generic type parameter needs to allow only certain shapes to be passed into the generic. To create this additional layer of specificity to your generic, you can put constraints on your parameter.

      Imagine you have a storage constraint where you are only allowed to store objects that have string values for all their properties. For that, you can create a function that takes any object and returns another object with the same keys as the original one, but with all their values transformed to strings. This function will be called stringifyObjectKeyValues.

      This function is going to be a generic function. This way, you are able to make the resulting object have the same shape as the original object. The function will look like this:

      function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
        return Object.keys(obj).reduce((acc, key) =>  ({
          ...acc,
          [key]: JSON.stringify(obj[key])
        }), {} as { [K in keyof T]: string })
      }
      

      In this code, stringifyObjectKeyValues uses the reduce array method to iterate over an array of the original keys, stringifying the values and adding them to a new array.

      To make sure the calling code is always going to pass an object to your function, you are using a type constraint on the generic type T, as shown in the following highlighted code:

      function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
        // ...
      }
      

      extends Record<string, any> is known as generic type constraint, and it allows you to specify that your generic type must be assignable to the type that comes after the extends keyword. In this case, Record<string, any> indicates an object with keys of type string and values of type any. You can make your type parameter extend any valid TypeScript type.

      When calling reduce, the return type of the reducer function is based on the initial value of the accumulator. The {} as { [K in keyof T]: string } code sets the type of the initial value of the accumulator to { [K in keyof T]: string } by using a type cast on an empty object, {}. The type { [K in keyof T]: string } creates a new type with the same keys as T, but with all the values set to have the string type. This is known as a mapped type, which this tutorial will explore further in a later section.

      The following code shows the implementation of your stringifyObjectKeyValues function:

      function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
        return Object.keys(obj).reduce((acc, key) =>  ({
          ...acc,
          [key]: JSON.stringify(obj[key])
        }), {} as { [K in keyof T]: string })
      }
      
      const stringifiedValues = stringifyObjectKeyValues({ a: "1", b: 2, c: true, d: [1, 2, 3]})
      

      The variable stringifiedValues will have the following type:

      {
        a: string;
        b: string;
        c: string;
        d: string;
      }
      

      This will ensure that the return value is consistent with the purpose of the function.

      This section covered multiple ways to use generics with functions, including directly assigning type parameters and making defaults and constraints to the parameter shape. Next, you’ll run through some examples of how generics can make interfaces and classes apply to more situations.

      Using Generics with Interfaces, Classes, and Types

      When creating interfaces and classes in TypeScript, it can be useful to use generic type parameters to set the shape of the resulting objects. For example, a class could have properties of different types depending on what is passed in to the constructor. In this section, you will see the syntax for declaring generic type parameters in classes and interfaces and examine a common use case in HTTP applications.

      Generic Interfaces and Classes

      To create a generic interface, you can add the type parameters list right after the interface name:

      interface MyInterface<T> {
        field: T
      }
      

      This declares an interface that has a property field whose type is determined by the type passed in to T.

      For classes, it’s almost the same syntax:

      class MyClass<T> {
        field: T
        constructor(field: T) {
          this.field = field
        }
      }
      

      One common use case of generic interfaces/classes is for when you have a field whose type depends on how the client code is using the interface/class. Say you have an HttpApplication class that is used to handle HTTP requests to your API, and that some context value is going to be passed around to every request handler. One such way to do this would be:

      class HttpApplication<Context> {
        context: Context
          constructor(context: Context) {
          this.context = context;
        }
      
        // ... implementation
      
        get(url: string, handler: (context: Context) => Promise<void>): this {
          // ... implementation
          return this;
        }
      }
      

      This class stores a context whose type is passed in as the type of the argument for the handler function in the get method. During usage, the parameter type passed to the get handler would correctly be inferred from what is passed to the class constructor.

      ...
      const context = { someValue: true };
      const app = new HttpApplication(context);
      
      app.get('/api', async () => {
        console.log(context.someValue)
      });
      

      In this implementation, TypeScript will infer the type of context.someValue as boolean.

      Generic Types

      Having now gone through some examples of generics in classes and interfaces, you can now move on to making generic custom types. The syntax for applying generics to types is similar to how they are applied to interfaces and classes. Take a look at the following code:

      type MyIdentityType<T> = T
      

      This generic type returns the type that is passed as the type parameter. Imagine you implemented this type with the following code:

      ...
      type B = MyIdentityType<number>
      

      In this case, the type B would be of type number.

      Generic types are commonly used to create helper types, especially when using mapped types. TypeScript provides many pre-built helper types. One such example is the Partial type, which takes a type T and returns another type with the same shape as T, but with all their fields set to optional. The implementation of Partial looks like this:

      type Partial<T> = {
        [P in keyof T]?: T[P];
      };
      

      The type Partial here takes in a type, iterates over its property types, then returns them as optional in a new type.

      Note: Since Partial is already built in to TypeScript, compiling this code into your TypeScript environment would re-declare Partial and throw an error. The implementation of Partial cited here is only for illustrative purposes.

      To see how powerful generic types are, imagine that you have an object literal that stores the shipping costs from a store to all other stores in your business distribution network. Each store will be identified by a three-character code, like this:

      {
        ABC: {
          ABC: null,
          DEF: 12,
          GHI: 13,
        },
        DEF: {
          ABC: 12,
          DEF: null,
          GHI: 17,
        },
        GHI: {
          ABC: 13,
          DEF: 17,
          GHI: null,
        },
      }
      

      This object is a collection of objects that represent the store location. Within each store location, there are properties that represent the cost to ship to other stores. For example, the cost to ship from ABC to DEF is 12. The shipping cost from one store to itself is null, as there will be no shipping at all.

      To ensure that locations for other stores have a consistent value and that a store shipping to itself is always null, you can create a generic helper type:

      type IfSameKeyThanParentTOtherwiseOtherType<Keys extends string, T, OtherType> = {
        [K in Keys]: {
          [SameThanK in K]: T;
        } &
          { [OtherThanK in Exclude<Keys, K>]: OtherType };
      };
      

      The type IfSameKeyThanParentTOtherwiseOtherType receives three generic types. The first one, Keys, are all the keys you want to make sure your object has. In this case, it is a union of all the stores’ codes. T is the type for when the nested object field has the same key as the key on the parent object, which in this case represents a store location shipping to itself. Finally, OtherType is the type for when the key is different, representing a store shipping to another store.

      You can use it like this:

      ...
      type Code="ABC" | 'DEF' | 'GHI'
      
      const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
        ABC: {
          ABC: null,
          DEF: 12,
          GHI: 13,
        },
        DEF: {
          ABC: 12,
          DEF: null,
          GHI: 17,
        },
        GHI: {
          ABC: 13,
          DEF: 17,
          GHI: null,
        },
      }
      

      This code is now enforcing the type shape. If you set any of the keys to an invalid value, TypeScript will give us an error:

      ...
      const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
        ABC: {
          ABC: 12,
          DEF: 12,
          GHI: 13,
        },
        DEF: {
          ABC: 12,
          DEF: null,
          GHI: 17,
        },
        GHI: {
          ABC: 13,
          DEF: 17,
          GHI: null,
        },
      }
      

      Since the shipping cost between ABC and itself is no longer null, TypeScript will throw the following error:

      Output

      Type 'number' is not assignable to type 'null'.(2322)

      You have now tried out using generics in interfaces, classes, and custom helper types. Next, you will explore further a topic that has already come up a few times in this tutorial: creating mapped types with generics.

      Creating Mapped Types with Generics

      When working with TypeScript, there are times when you will need to create a type that should have the same shape as another type. This means that it should have the same properties, but with the type of the properties set to something different. For this situation, using mapped types can reuse the initial type shape and reduce repeated code in your application.

      In TypeScript, this structure is known as a mapped type and relies on generics. In this section, you will see how to create a mapped type.

      Imagine you want to create a type that, given another type, returns a new type where all the properties are set to have a boolean value. You could create this type with the following code:

      type BooleanFields<T> = {
        [K in keyof T]: boolean;
      }
      

      In this type, you are using the syntax [K in keyof T] to specify the properties that the new type will have. The keyof T operator is used to return a union with the name of all the properties available in T. You are then using the K in syntax to designate that the properties of the new type are all the properties currently available in the union type returned by keyof T.

      This creates a new type called K, which is bound to the name of the current property. This can be used to access the type of this property in the original type using the syntax T[K]. In this case, you are setting the type of the properties to be a boolean.

      One usage scenario for this BooleanFields type is creating an options object. Imagine you have a database model, like a User. When getting a record for this model from the database, you will also allow passing an object that specifies which fields to return. This object would have the same properties as the model, but with the type set to a boolean. Passing true in a field means you want it to be returned and false that you want it to be omitted.

      You could use your BooleanFields generic on the existing model type to return a new type with the same shape as the model, but with all the fields set to have a boolean type, like in the following highlighted code:

      type BooleanFields<T> = {
        [K in keyof T]: boolean;
      };
      
      type User = {
        email: string;
        name: string;
      }
      
      type UserFetchOptions = BooleanFields<User>;
      

      In this example, UserFetchOptions would be the same as creating it like this:

      type UserFetchOptions = {
        email: boolean;
        name: boolean;
      }
      

      When creating mapped types, you can also provide modifiers for the fields. One such example is the existing generic type available in TypeScript called Readonly<T>. The Readonly<T> type returns a new type where all the properties of the passed type are set to be readonly properties. The implementation of this type looks like this:

      type Readonly<T> = {
        readonly [K in keyof T]: T[K]
      }
      

      Note: Since Readonly is already built in to TypeScript, compiling this code into your TypeScript environment would re-declare Readonly and throw an error. The implementation of Readonly cited here is only for illustrative purposes.

      Notice the modifier readonly that is added as a prefix to the [K in keyof T] part in this code. Currently, the two available modifiers that can be used in mapped types are the readonly modifier, which must be added as a prefix to the property, and the ? modifier, which can be added as a suffix to the property. The ? modifier marks the field as optional. Both modifiers can receive a special prefix to specify if the modifier should be removed (-) or added (+). If only the modifier is provided, + is assumed.

      Now that you can use mapped types to create new types based on type shapes that you’ve already created, you can move on to the final use case for generics: conditional typing.

      Creating Conditional Types with Generics

      In this section, you will try out another helpful feature of generics in TypeScript: creating conditional types. First, you will run through the basic structure of conditional typing. Then you will explore an advanced use case by creating a conditional type that omits nested fields of an object type based on dot notation.

      Basic Structure of Conditional Typing

      Conditional types are generic types that have a different resulting type depending on some condition. For example, take a look at the following generic type IsStringType<T>:

      type IsStringType<T> = T extends string ? true : false;
      

      In this code, you are creating a new generic type called IsStringType that receives a single type parameter, T. Inside the definition of your type, you are using a syntax that looks like a conditional expression using the ternary operator in JavaScript: T extends string ? true : false. This conditional expression is checking if the type T extends the type string. If it does, the resulting type will be the exact type true; otherwise, it will be set to the type false.

      Note: This conditional expression is evaluated during compilation. TypeScript only works with types, so make sure to always read the identifiers within a type declaration as types, not as values. In this code, you are using the exact type of each boolean value, true and false.

      To try out this conditional type, pass some types as its type parameter:

      type IsStringType<T> = T extends string ? true : false;
      
      type A = "abc";
      type B = {
        name: string;
      };
      
      type ResultA = IsStringType<A>;
      type ResultB = IsStringType<B>;
      

      In this code, you are creating two types, A and B. The type A is the type of the string literal "abc", while the type B is the type of an object that has a property called name of type string. You are then using both types with your IsStringType conditional type and storing the resulting type into two new types, ResultA and ResultB.

      If you check the resulting type of ResultA and ResultB, you will notice that the ResultA type is set to the exact type true and that the ResultB type is set to false. This is correct, as A does extend the string type and B does not extend the string type, as it is set to the type of an object with a single name property of type string.

      One useful feature of conditional types is that it allows you to infer type information inside the extends clause using the special keyword infer. This new type can then be used in the true branch of the condition. One possible usage for this feature is retrieving the return type of any function type.

      Write the following GetReturnType type to illustrate this:

      type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
      

      In this code, you are creating a new generic type, which is a conditional type called GetReturnType. This generic type accepts a single type parameter, T. Inside the type declaration itself, you are checking if the type T extends a type matching a function signature that accepts a variadic number of arguments (including zero), and you are then inferring the return type of that function creating a new type U, which is available to be used inside the true branch of the condition. The type of U is going to be bound to the type of the return value of the passed function. If the passed type T is not a function, then the code will return the type never.

      Use your type with the following code:

      type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
      
      function someFunction() {
        return true;
      }
      
      type ReturnTypeOfSomeFunction = GetReturnType<typeof someFunction>;
      

      In this code, you are creating a function called someFunction, which returns true. You are then using the typeof operator to pass in the type of this function to the GetReturnType generic and storing the resulting type in the ReturnTypeOfSomeFunction type.

      As the type of your someFunction variable is a function, the conditional type would evaluate the true branch of the condition. This will return the type U as the result. The type U was inferred from the return type of the function, which in this case is a boolean. If you check the type of ReturnTypeOfSomeFunction, you will find that it is correctly set to have the boolean type.

      Advanced Conditional Type Use Case

      Conditional types are one of the most flexible features available in TypeScript and allow for the creation of some advanced utility types. In this section, you will explore one of these use cases by creating a conditional type called NestedOmit<T, KeysToOmit>. This utility type will be able to omit fields from an object, just like the existing Omit<T, KeysToOmit> utility type, but will also allow omitting nested fields by using dot notation.

      Using your new NestedOmit<T, KeysToOmit> generic, you will be able to use the type as shown in the following example:

      type SomeType = {
        a: {
          b: string,
          c: {
            d: number;
            e: string[]
          },
          f: number
        }
        g: number | string,
        h: {
          i: string,
          j: number,
        },
        k: {
          l: number,<F3>
        }
      }
      
      type Result = NestedOmit<SomeType, "a.b" | "a.c.e" | "h.i" | "k">;
      

      This code declares a type named SomeType that has a multi-level structure of nested properties. Using your NestedOmit generic, you pass in the type, then list the keys of the properties you’d like to omit. Notice how you can use dot notation in the second type parameter to identify the keys to omit. The resulting type is then stored in Result.

      Constructing this conditional type will use many features available in TypeScript, like template literal types, generics, conditional types, and mapped types.

      To try out this generic, start by creating a generic type called NestedOmit that accepts two type parameters:

      type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>
      

      The first type parameter is called T, which must be a type that is assignable to the Record<string, any> type. This will be the type of the object you want to omit properties from. The second type parameter is called KeysToOmit, which must be of type string. You will use this to specify the keys you want to omit from your type T.

      Next, check if KeysToOmit is assignable to the type ${infer KeyPart1}.${infer KeyPart2} by adding the following highlighted code:

      type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
        KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
      

      Here, you are using a template literal string type while taking advantage of conditional types to infer two other types inside the template literal itself. By inferring two parts of the template literal string type, you are splitting the string into two other strings. The first part will be assigned to the type KeyPart1 and will contain everything before the first dot. The second part will be assigned to the type KeyPart2 and will contain everything after the first dot. If you passed "a.b.c" as the KeysToOmit, initially KeyPart1 would be set to the exact string type "a", and KeyPart2 would be set to "b.c".

      Next you will add the ternary operator to define the first true branch of the condition:

      type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
        KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
          ?
            KeyPart1 extends keyof T
      

      This uses KeyPart1 extends keyof T to check if KeyPart1 is a valid property of the given type T. In case you do have a valid key, add the following code to make the condition evaluate to an intersection between the two types:

      type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
        KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
          ?
            KeyPart1 extends keyof T
            ?
              Omit<T, KeyPart1>
              & {
                [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
              }
      

      Omit<T, KeyPart1> is a type built by using the Omit helper shipped by default with TypeScript. At this point, KeyPart1 is not in dot notation: It is going to contain the exact name of a field that contains nested fields that you want to omit from the original type. Because of this, you can safely use the existing utility type.

      You are using Omit to remove some nested fields that are inside T[KeyPart1], and to do that, you have to rebuild the type of T[KeyPart1]. To avoid rebuilding the whole T type, you use Omit to remove just KeyPart1 from T, preserving other fields. Then you are rebuilding T[KeyPart1] in the type in the next part.

      [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2> is a mapped type where the properties are the ones assignable to KeyPart1, which means the part you just extracted from KeysToOmit. This is the parent of the fields you want to remove. If you passed a.b.c, during the first evaluation of your condition it would be NewKeys in "a". You are then setting the type of this property to be the result of recursively calling your NestedOmit utility type, but now passing as the first type parameter the type of this property inside T by using T[NewKeys], and passing as the second type parameter the remaining keys in dot notation, available in KeyPart2.

      In the false branch of the internal condition, you return the current type bound to T, as if KeyPart1 is not a valid key of T:

      type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
        KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
          ?
            KeyPart1 extends keyof T
            ?
              Omit<T, KeyPart1>
              & {
                [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
              }
            : T
      

      This branch of the conditional means you are trying to omit a field that does not exist in T. In this case, there is no need to go any further.

      Finally, in the false branch of the outer condition, use the existing Omit utility type to omit KeysToOmit from Type:

      type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
        KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
          ?
            KeyPart1 extends keyof T
            ?
              Omit<T, KeyPart1>
              & {
                [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
              }
            : T
          : Omit<T, KeysToOmit>;
      

      If the condition KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}` is false, it means KeysToOmit is not using dot notation, and thus you can use the existing Omit utility type.

      Now, to use your new NestedOmit conditional type, create a new type called NestedObject:

      type NestedObject = {
        a: {
          b: {
            c: number;
            d: number;
          };
          e: number;
        };
        f: number;
      };
      

      Then call NestedOmit on it to omit the nested field available at a.b.c:

      type Result = NestedOmit<NestedObject, "a.b.c">;
      

      On the first evaluation of the conditional type, the outer condition would be true, as the string literal type "a.b.c" is assignable to the template literal type `${infer KeyPart1}.${infer KeyPart2}`. In this case, KeyPart1 would be inferred as the string literal type "a" and KeyPart2 would be inferred as the remaining of the string, in this case "b.c".

      The inner condition is now going to be evaluated. This will evaluate to true, as KeyPart1 at this point is a key of T. KeyPart1 now is "a", and T does have a property "a":

      type NestedObject = {
        a: {
          b: {
            c: number;
            d: number;
          };
          e: number;
        };
        f: number;
      };
      

      Moving forward with the evaluation of the condition, you are now inside the inner true branch. This builds a new type that is an intersection of two other types. The first type is the result of using the Omit utility type on T to omit the fields that are assignable to KeyPart1, in this case the a field. The second type is a new type you are building by calling NestedOmit recursively.

      If you go through the next evaluation of NestedOmit, for the first recursive call, the intersection type is now building a type to use as the type of the a field. This recreates the a field without the nested fields you need to omit.

      In the final evaluation of NestedOmit, the first condition would return false, as the passed string type is just "c" now. When this happens, you omit the field from the object with the built-in helper. This would return the type for the b field, which is the original type with c omitted. The evaluation now ends and TypeScript returns the new type you want to use, with the nested field omitted.

      Conclusion

      In this tutorial, you explore generics as they apply to functions, interfaces, classes, and custom types. You also used generics to create mapped and conditional types. Each of these makes generics a powerful tool you have at your disposal when using TypeScript. Using them correctly will save you from repeating code over and over again, and will make the types you have written more flexible. This is especially true if you are a library author and are planning to make your code legible for a wide audience.

      For more tutorials on TypeScript, check out our How To Code in TypeScript series page.



      Source link

      How To Use Interfaces in TypeScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      TypeScript is an extension of the JavaScript language that uses JavaScript’s runtime with a compile-time type checker.

      TypeScript offers multiple ways to represent objects in your code, one of which is using interfaces. Interfaces in TypeScript have two usage scenarios: you can create a contract that classes must follow, such as the members that those classes must implement, and you can also represent types in your application, just like the normal type declaration. (For more about types, check out How to Use Basic Types in TypeScript and How to Create Custom Types in TypeScript.)

      You may notice that interfaces and types share a similar set of features; in fact, one can almost always replace the other. The main difference is that interfaces may have more than one declaration for the same interface, which TypeScript will merge, while types can only be declared once. You can also use types to create aliases of primitive types (such as string and boolean), which interfaces cannot do.

      Interfaces in TypeScript are a powerful way to represent type structures. They allow you to make the usage of those structures type-safe and document them simultaneously, directly improving the developer experience.

      In this tutorial, you will create interfaces in TypeScript, learn how to use them, and understand the differences between normal types and interfaces. You will try out different code samples, which you can follow in your own TypeScript environment or the TypeScript Playground, an online environment that allows you to write TypeScript directly in the browser.

      Prerequisites

      To follow this tutorial, you will need:

      • An environment in which you can execute TypeScript programs to follow along with the examples. To set this up on your local machine, you will need the following.
      • If you do not wish to create a TypeScript environment on your local machine, you can use the official TypeScript Playground to follow along.

      • You will need sufficient knowledge of JavaScript, especially ES6+ syntax, such as destructuring, rest operators, and imports/exports. If you need more information on these topics, reading our How To Code in JavaScript series is recommended.

      • This tutorial will reference aspects of text editors that support TypeScript and show in-line errors. This is not necessary to use TypeScript but does take more advantage of TypeScript features. To gain the benefit of these, you can use a text editor like Visual Studio Code, which has full support for TypeScript out of the box. You can also try out these benefits in the TypeScript Playground.

      All examples shown in this tutorial were created using TypeScript version 4.2.2.

      Creating and Using Interfaces in TypeScript

      In this section, you will create interfaces using different features available in TypeScript. You will also learn how to use the interfaces you created.

      Interfaces in TypeScript are created by using the interface keyword followed by the name of the interface, and then a {} block with the body of the interface. For example, here is a Logger interface:

      interface Logger {
        log: (message: string) => void;
      }
      

      Similar to creating a normal type using the type declaration, you specify the fields of the type, and their type, in the {}:

      interface Logger {
        log: (message: string) => void;
      }
      

      The Logger interface represents an object that has a single property called log. This property is a function that accepts a single parameter of type string and returns void.

      You can use the Logger interface as any other type. Here is an example creating an object literal that matches the Logger interface:

      interface Logger {
        log: (message: string) => void;
      }
      
      const logger: Logger = {
        log: (message) => console.log(message),
      };
      

      Values using the Logger interface as their type must have the same members as those specified in the Logger interface declaration. If some members are optional, they may be omitted.

      Since values must follow what is declared in the interface, adding extraneous fields will cause a compilation error. For example, in the object literal, try adding a new property that is missing from the interface:

      interface Logger {
        log: (message: string) => void;
      }
      
      const logger: Logger = {
        log: (message) => console.log(message),
        otherProp: true,
      };
      

      In this case, the TypeScript Compiler would emit error 2322, as this property does not exist in the Logger interface declaration:

      Output

      Type '{ log: (message: string) => void; otherProp: boolean; }' is not assignable to type 'Logger'. Object literal may only specify known properties, and 'otherProp' does not exist in type 'Logger'. (2322)

      Similar to using normal type declarations, properties can be turned into an optional property by appending ? to their name.

      Extending Other Types

      When creating interfaces, you can extend from different object types, allowing your interfaces to include all the type information from the extended types. This enables you to write small interfaces with a common set of fields and use them as building blocks to create new interfaces.

      Imagine you have a Clearable interface, such as this one:

      interface Clearable {
        clear: () => void;
      }
      

      You could then create a new interface that extends from it, inheriting all its fields. In the following example, the interface Logger is extending from the Clearable interface. Notice the highlighted lines:

      interface Clearable {
        clear: () => void;
      }
      
      interface Logger extends Clearable {
        log: (message: string) => void;
      }
      

      The Logger interface now also has a clear member, which is a function that accepts no parameters and returns void. This new member is inherited from the Clearable interface. It is the same as if we did this:

      interface Logger {
        log: (message: string) => void;
        clear: () => void;
      }
      

      When writing lots of interfaces with a common set of fields, you can extract them to a different interface and change your interfaces to extend from the new interface you created.

      Returning to the Clearable example used previously, imagine that your application needs a different interface, such as the following StringList interface, to represent a data structure that holds multiple strings:

      interface StringList {
        push: (value: string) => void;
        get: () => string[];
      }
      

      By making this new StringList interface extend the existing Clearable interface, you are specifying that this interface also has the members set in the Clearable interface, adding the clear property to the type definition of the StringList interface:

      interface StringList extends Clearable {
        push: (value: string) => void;
        get: () => string[];
      }
      

      Interfaces can extend from any object type, such as interfaces, normal types, and even classes.

      Interfaces with Callable Signature

      If the interface is also callable (that is, it is also a function), you can convey that information in the interface declaration by creating a callable signature.

      A callable signature is created by adding a function declaration inside the interface that is not bound to any member and by using : instead of => when setting the return type of the function.

      As an example, add a callable signature to your Logger interface, as in the highlighted code below:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      

      Notice that the callable signature resembles the type declaration of an anonymous function, but in the return type you are using : instead of =>. This means that any value bound to the Logger interface type can be called directly as a function.

      To create a value that matches your Logger interface, you need to consider the requirements of the interface:

      1. It must be callable.
      2. It must have a property called log that is a function accepting a single string parameter.

      Let’s create a variable called logger that is assignable to the type of your Logger interface:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = (message: string) => {
        console.log(message);
      }
      

      To match the Logger interface, the value must be callable, which is why you assign the logger variable to a function:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = (message: string) => {
        console.log(message);
      }
      

      You are then adding the log property to the logger function:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = (message: string) => {
        console.log(message);
      }
      

      This is required by the Logger interface. Values bound to the Logger interface must also have a log property that is a function accepting a single string parameter and that returns void.

      If you did not include the log property, the TypeScript Compiler would give you error 2741:

      Output

      Property 'log' is missing in type '(message: string) => void' but required in type 'Logger'. (2741)

      The TypeScript Compiler would emit a similar error if the log property in the logger variable had an incompatible type signature, like setting it to true:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = true;
      

      In this case, the TypeScript Compiler would show error 2322:

      Output

      Type 'boolean' is not assignable to type '(message: string) => void'. (2322)

      A nice feature of setting variables to have a specific type, in this case setting the logger variable to have the type of the Logger interface, is that TypeScript can now infer the type of the parameters of both the logger function and the function in the log property.

      You can check that by removing the type information from the argument of both functions. Notice that in the highlighted code below, the message parameters do not have a type:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      
      const logger: Logger = (message) => {
        console.log(message);
      }
      logger.log = (message) => {
        console.log(message);
      }
      

      And in both cases, your editor should still be able to show that the type of the parameter is a string, as this is the type expected by the Logger interface.

      Interfaces with Index Signatures

      You can add an index signature to your interface, just like you can with normal types, thus allowing the interface to have an unlimited number of properties.

      For example, if you wanted to create a DataRecord interface that has an unlimited number of string fields, you could use the following highlighted index signature:

      interface DataRecord {
        [key: string]: string;
      }
      

      You can then use the DataRecord interface to set the type of any object that has multiple parameters of type string:

      interface DataRecord {
        [key: string]: string;
      }
      
      const data: DataRecord = {
        fieldA: "valueA",
        fieldB: "valueB",
        fieldC: "valueC",
        // ...
      };
      

      In this section, you created interfaces using different features available in TypeScript and learned how to use the interfaces you created. In the next section, you’ll learn more about the differences between type and interface declarations, and gain practice with declaration merging and module augmentation.

      Differences Between Types and Interfaces

      So far, you have seen that the interface declaration and the type declaration are similar, having almost the same set of features.

      For example, you created a Logger interface that extended from a Clearable interface:

      interface Clearable {
        clear: () => void;
      }
      
      interface Logger extends Clearable {
        log: (message: string) => void;
      }
      

      The same type representation can be replicated by using two type declarations:

      type Clearable = {
        clear: () => void;
      }
      
      type Logger = Clearable & {
        log: (message: string) => void;
      }
      

      As shown in the previous sections, the interface declaration can be used to represent a variety of objects, from functions to complex objects with an unlimited number of properties. This is also possible with type declarations, even extending from other types, as you can intersect multiple types together using the intersection operator &.

      Since type declarations and interface declarations are so similar, you’ll need to consider the specific features unique to each one and be consistent in your codebase. Pick one to create type representations in your codebase, and only use the other one when you need a specific feature only available to it.

      For example, the type declaration has a few features that the interface declaration lacks, such as:

      • Union types.
      • Mapped types.
      • Alias to primitive types.

      One of the features available only for the interface declaration is declaration merging, which you will learn about in the next section. It is important to note that declaration merging may be useful if you are writing a library and want to give the library users the power to extend the types provided by the library, as this is not possible with type declarations.

      Declaration Merging

      TypeScript can merge multiple declarations into a single one, enabling you to write multiple declarations for the same data structure and having them bundled together by the TypeScript Compiler during compilation as if they were a single type. In this section, you will see how this works and why it is helpful when using interfaces.

      Interfaces in TypeScript can be re-opened; that is, multiple declarations of the same interface can be merged. This is useful when you want to add new fields to an existing interface.

      For example, imagine that you have an interface named DatabaseOptions like the following one:

      interface DatabaseOptions {
        host: string;
        port: number;
        user: string;
        password: string;
      }
      

      This interface is going to be used to pass options when connecting to a database.

      Later in the code, you declare an interface with the same name but with a single string field called dsnUrl, like this one:

      interface DatabaseOptions {
        dsnUrl: string;
      }
      

      When the TypeScript Compiler starts reading your code, it will merge all declarations of the DatabaseOptions interface into a single one. From the TypeScript Compiler point of view, DatabaseOptions is now:

      interface DatabaseOptions {
        host: string;
        port: number;
        user: string;
        password: string;
        dsnUrl: string;
      }
      

      The interface includes all the fields you initially declared, plus the new field dsnUrl that you declared separately. Both declarations have been merged.

      Module Augmentation

      Declaration merging is helpful when you need to augment existing modules with new properties. One use-case for that is when you are adding more fields to a data structure provided by a library. This is relatively common with the Node.js library called express, which allows you to create HTTP servers.

      When working with express, a Request and a Response object are passed to your request handlers (functions responsible for providing a response to a HTTP request). The Request object is commonly used to store data specific to a particular request. For example, you could use it to store the logged user that made the initial HTTP request:

      const myRoute = (req: Request, res: Response) => {
        res.json({ user: req.user });
      }
      

      Here, the request handler sends back to the client a json with the user field set to the logged user. The logged user is added to the request object in another place in the code, using an express middleware responsible for user authentication.

      The type definitions for the Request interface itself does not have a user field, so the above code would give the type error 2339:

      Property 'user' does not exist on type 'Request'. (2339)
      

      To fix that, you have to create a module augmentation for the express package, taking advantage of declaration merging to add a new property to the Request interface.

      If you check the type of the Request object in the express type declaration, you will notice that it is an interface added inside a global namespace called Express, as shown in documentation from the DefinitelyTyped repository:

      declare global {
          namespace Express {
              // These open interfaces may be extended in an application-specific manner via declaration merging.
              // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
              interface Request {}
              interface Response {}
              interface Application {}
          }
      }
      

      Note: Type declaration files are files that only contain type information. The DefinitelyTyped repository is the official repository to submit type declarations for packages that do not have one. The @types/<package> packages available on npm are published from this repository.

      To use module augmentation to add a new property to the Request interface, you have to replicate the same structure in a local type declaration file. For example, imagine that you created a file named express.d.ts like the following one and then added it to the types option of your tsconfig.json:

      import 'express';
      
      declare global {
        namespace Express {
          interface Request {
            user: {
              name: string;
            }
          }
        }
      }
      

      From the TypeScript Compiler point of view, the Request interface has a user property, with their type set to an object having a single property called name of type string. This happens because all the declarations for the same interface are merged.

      Suppose you are creating a library and want to give the users of your library the option to augment the types provided by your own library, like you did above with express. In that case, you are required to export interfaces from your library, as normal type declarations do not support module augmentation.

      Conclusion

      In this tutorial, you have written multiple TypeScript interfaces to represent various data structures, discovered how you can use different interfaces together as building blocks to create powerful types, and learned about the differences between normal type declarations and interfaces. You can now start writing interfaces for data structures in your codebase, allowing you to have type-safe code as well as documentation.

      For more tutorials on TypeScript, check out our How To Code in TypeScript series page.



      Source link

      How To Use Decorators in TypeScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      TypeScript is an extension of the JavaScript language that uses JavaScript’s runtime with a compile-time type checker. This combination allows developers to use the full JavaScript ecosystem and language features, while also adding optional static type-checking, enums, classes, and interfaces on top of it. One of those extra features is decorator support.

      Decorators are a way to decorate members of a class, or a class itself, with extra functionality. When you apply a decorator to a class or a class member, you are actually calling a function that is going to receive details of what is being decorated, and the decorator implementation will then be able to transform the code dynamically, adding extra functionality, and reducing boilerplate code. They are a way to have metaprogramming in TypeScript, which is a programming technique that enables the programmer to create code that uses other code from the application itself as data.

      Currently, there is a stage-2 proposal adding decorators to the ECMAScript standard. As it is not a JavaScript feature yet, TypeScript offers its own implementation of decorators, under an experimental flag.

      This tutorial will show you how create your own decorators in TypeScript for classes and class members, and also how to use them. It will lead you through different code samples, which you can follow in your own TypeScript environment or the TypeScript Playground, an online environment that allows you to write TypeScript directly in the browser.

      Prerequisites

      To follow this tutorial, you will need:

      • An environment in which you can execute TypeScript programs to follow along with the examples. To set this up on your local machine, you will need the following.
      • If you do not wish to create a TypeScript environment on your local machine, you can use the official TypeScript Playground to follow along.
      • You will need sufficient knowledge of JavaScript, especially ES6+ syntax, such as destructuring, rest operators, and imports/exports. If you need more information on these topics, reading our How To Code in JavaScript series is recommended.
      • This tutorial will reference aspects of text editors that support TypeScript and show in-line errors. This is not necessary to use TypeScript but does take more advantage of TypeScript features. To gain the benefit of these, you can use a text editor like Visual Studio Code, which has full support for TypeScript out of the box. You can also try out these benefits in the TypeScript Playground.

      All examples shown in this tutorial were created using TypeScript version 4.2.2.

      Enabling Decorators Support in TypeScript

      Currently, decorators are still an experimental feature in TypeScript, and as such, it must be enabled first. In this section, you will see how to enable decorators in TypeScript, depending on the way you are working with TypeScript.

      TypeScript Compiler CLI

      To enable decorators support when using the TypeScript Compiler CLI (tsc) the only extra step needed is to pass an additional flag --experimentalDecorators:

      tsc --experimentalDecorators
      

      tsconfig.json

      When working in a project that has a tsconfig.json file, to enable experimental decorators you must add the experimentalDecorators property to the compilerOptions object:

      {
        "compilerOptions": {
          "experimentalDecorators": true
        }
      }
      

      In the TypeScript Playground, decorators are enabled by default.

      Using Decorator Syntax

      In this section, you will apply decorators in TypeScript classes.

      In TypeScript, you can create decorators using the special syntax @expression, where expression is a function that will be called automatically during runtime with details about the target of the decorator.

      The target of a decorator depends on where you add them. Currently, decorators can be added to the following components of a class:

      • Class declaration itself
      • Properties
      • Accessors
      • Methods
      • Parameters

      For example, let’s say you have a decorator called sealed which calls Object.seal in a class. To use your decorator you could write the following:

      @sealed
      class Person {}
      

      Notice in the highlighted code that you added the decorator right before the target of your sealed decorator, in this case, the Person class declaration.

      The same is valid for all other kinds of decorators:

      @classDecorator
      class Person {
        @propertyDecorator
        public name: string;
      
        @accessorDecorator
        get fullName() {
          // ...
        }
      
        @methodDecorator
        printName(@parameterDecorator prefix: string) {
          // ...
        }
      }
      

      To add multiple decorators, you add them together, one after the other:

      @decoratorA
      @decoratorB
      class Person {}
      

      Creating Class Decorators in TypeScript

      In this section, you will go through the steps to create class decorators in TypeScript.

      For a decorator called @decoratorA, you tell TypeScript it should call the function decoratorA. The decoratorA function will be called with details about how you used the decorator in your code. For example, if you applied the decorator to a class declaration, the function will receive details about the class. This function must be in scope for your decorator to work.

      To create your own decorator, you have to create a function with the same name as your decorator. That is, to create the sealed class decorator you saw in the previous section, you have to create a sealed function that receives a specific set of parameters. Let’s do exactly that:

      @sealed
      class Person {}
      
      function sealed(target: Function) {
        Object.seal(target);
        Object.seal(target.prototype);
      }
      

      The parameter(s) passed to the decorator will depend on where the decorator will be used. The first parameter is commonly called target.

      The sealed decorator will be used only on class declarations, so your function will receive a single parameter, the target, which will be of type Function. This will be the constructor of the class that the decorator was applied to.

      In the sealed function you are then calling Object.seal on the target, which is the class constructor, and also on their prototype. When you do that no new properties can be added to the class constructor or their property, and the existing ones will be marked as non-configurable.

      It is important to remember that it is currently not possible to extend the TypeScript type of the target when using decorators. This means, for example, you are not able to add a new field to a class using a decorator and make it type-safe.

      If you returned a value in the sealed class decorator, this value will become the new constructor function for the class. This is useful if you want to completely overwrite the class constructor.

      You have created your first decorator, and used it with a class. In the next section you will learn how to create decorator factories.

      Creating Decorator Factories

      Sometimes you will need to pass additional options to the decorator when applying it, and for that, you have to use decorator factories. In this section, you will learn how to create those factories and use them.

      Decorator factories are functions that return another function. They receive this name because they are not the decorator implementation itself. Instead, they return another function responsible for the implementation of the decorator and act as a wrapper function. They are useful in making decorators customizable, by allowing the client code to pass options to the decorators when using them.

      Let’s imagine you have a class decorator called decoratorA and you want to add an option that can be set when calling the decorator, like a boolean flag. You can achieve this by writing a decorator factory similar to the following one:

      const decoratorA = (someBooleanFlag: boolean) => {
        return (target: Function) => {
        }
      }
      

      Here, your decoratorA function returns another function with the implementation of the decorator. Notice how the decorator factory receives a boolean flag as its only parameter:

      const decoratorA = (someBooleanFlag: boolean) => {
        return (target: Function) => {
        }
      }
      

      You are able to pass the value of this parameter when using the decorator. See the highlighted code in the following example:

      const decoratorA = (someBooleanFlag: boolean) => {
        return (target: Function) => {
        }
      }
      
      @decoratorA(true)
      class Person {}
      

      Here, when you use the decoratorA decorator, the decorator factory is going to be called with the someBooleanFlag parameter set to true. Then the decorator implementation itself will run. This allows you to change the behavior of your decorator based on how it was used, making your decorators easy to customize and re-use through your application.

      Notice that you are required to pass all the parameters expected by the decorator factory. If you simply applied the decorator without passing any parameters, like in the following example:

      const decoratorA = (someBooleanFlag: boolean) => {
        return (target: Function) => {
        }
      }
      
      @decoratorA
      class Person {}
      

      The TypeScript Compiler will give you two errors, which may vary depending on the type of the decorator. For class decorators the errors are 1238 and 1240:

      Output

      Unable to resolve signature of class decorator when called as an expression. Type '(target: Function) => void' is not assignable to type 'typeof Person'. Type '(target: Function) => void' provides no match for the signature 'new (): Person'. (1238) Argument of type 'typeof Person' is not assignable to parameter of type 'boolean'. (2345)

      You just created a decorator factory that is able to receive parameters and change their behavior based on these parameters. In the next step you will learn how to create property decorators.

      Creating Property Decorators

      Class properties are another place you can use decorators. In this section you will take a look at how to create them.

      Any property decorator receives the following parameters:

      • For static properties, the constructor function of the class. For all the other properties, the prototype of the class.
      • The name of the member.

      Currently, there is no way to obtain the property descriptor as a parameter. This is due to the way that property decorators are initialized in TypeScript.

      Here is a decorator function that will print the name of the member to the console:

      const printMemberName = (target: any, memberName: string) => {
        console.log(memberName);
      };
      
      class Person {
        @printMemberName
        name: string = "Jon";
      }
      

      When you run the above TypeScript code, you will see the following printed in the console:

      Output

      name

      You can use property decorators to override the property being decorated. This can be done by using Object.defineProperty along with a new setter and getter for the property. Let’s see how you can create a decorator named allowlist, which only allows a property to be set to values present in a static allowlist:

      const allowlist = ["Jon", "Jane"];
      
      const allowlistOnly = (target: any, memberName: string) => {
        let currentValue: any = target[memberName];
      
        Object.defineProperty(target, memberName, {
          set: (newValue: any) => {
            if (!allowlist.includes(newValue)) {
              return;
            }
            currentValue = newValue;
          },
          get: () => currentValue
        });
      };
      

      First, you are creating a static allowlist at the top of the code:

      const allowlist = ["Jon", "Jane"];
      

      You are then creating the implementation of the property decorator:

      const allowlistOnly = (target: any, memberName: string) => {
        let currentValue: any = target[memberName];
      
        Object.defineProperty(target, memberName, {
          set: (newValue: any) => {
            if (!allowlist.includes(newValue)) {
              return;
            }
            currentValue = newValue;
          },
          get: () => currentValue
        });
      };
      

      Notice how you are using any as the type of the target:

      const allowlistOnly = (target: any, memberName: string) => {
      

      For property decorators, the type of the target parameter can be either the constructor of the class or the prototype of the class, it is easier to use any in this situation.

      In the first line of your decorator implementation you are storing the current value of the property being decorated to the currentValue variable:

        let currentValue: any = target[memberName];
      

      For static properties, this will be set to their default value, if any. For non-static properties, this will always be undefined. This is because at runtime, in the compiled JavaScript code, the decorator runs before the instance property is set to its default value.

      You are then overriding the property by using Object.defineProperty:

        Object.defineProperty(target, memberName, {
          set: (newValue: any) => {
            if (!allowlist.includes(newValue)) {
              return;
            }
            currentValue = newValue;
          },
          get: () => currentValue
        });
      

      The Object.defineProperty call has a getter and a setter. The getter returns the value stored in the currentValue variable. The setter will set the value of currentVariable to newValue if it is within the allowlist.

      Let’s use the decorator you just wrote. Create the following Person class:

      class Person {
        @allowlistOnly
        name: string = "Jon";
      }
      

      You will now create a new instance of your class, and test setting and getting the name instance property:

      const allowlist = ["Jon", "Jane"];
      
      const allowlistOnly = (target: any, memberName: string) => {
        let currentValue: any = target[memberName];
      
        Object.defineProperty(target, memberName, {
          set: (newValue: any) => {
            if (!allowlist.includes(newValue)) {
              return;
            }
            currentValue = newValue;
          },
          get: () => currentValue
        });
      };
      
      class Person {
        @allowlistOnly
        name: string = "Jon";
      }
      
      const person = new Person();
      console.log(person.name);
      
      person.name = "Peter";
      console.log(person.name);
      
      person.name = "Jane";
      console.log(person.name);
      

      Running the code you should see the following output:

      Output

      Jon Jon Jane

      The value is never set to Peter, as Peter is not in the allowlist.

      What if you wanted to make your code a bit more re-usable, allowing the allowlist to be set when applying the decorator? This is a great use case for decorator factories. Let’s do exactly that, by turning your allowlistOnly decorator into a decorator factory:

      const allowlistOnly = (allowlist: string[]) => {
        return (target: any, memberName: string) => {
          let currentValue: any = target[memberName];
      
          Object.defineProperty(target, memberName, {
            set: (newValue: any) => {
              if (!allowlist.includes(newValue)) {
                return;
              }
              currentValue = newValue;
            },
            get: () => currentValue
          });
        };
      }
      

      Here you wrapped your previous implementation into another function, a decorator factory. The decorator factory receives a single parameter called allowlist, which is an array of strings.

      Now to use your decorator, you must pass the allowlist, like in the following highlighted code:

      class Person {
        @allowlistOnly(["Claire", "Oliver"])
        name: string = "Claire";
      }
      

      Try running a code similar to the previous one you wrote, but with the new changes:

      const allowlistOnly = (allowlist: string[]) => {
        return (target: any, memberName: string) => {
          let currentValue: any = target[memberName];
      
          Object.defineProperty(target, memberName, {
            set: (newValue: any) => {
              if (!allowlist.includes(newValue)) {
                return;
              }
              currentValue = newValue;
            },
            get: () => currentValue
          });
        };
      }
      
      class Person {
        @allowlistOnly(["Claire", "Oliver"])
        name: string = "Claire";
      }
      
      const person = new Person();
      console.log(person.name);
      person.name = "Peter";
      console.log(person.name);
      person.name = "Oliver";
      console.log(person.name);
      

      The code should give you the following output:

      Output

      Claire Claire Oliver

      Showing that it is working as expected, person.name is never set to Peter, as Peter is not in the given allowlist.

      Now that you created your first property decorator using both a normal decorator function and a decorator factory, it is time to take a look at how to create decorators for class accessors.

      Creating Accessor Decorators

      In this section, you will take a look at how to decorate class accessors.

      Just like property decorators, decorators used in an accessor receives the following parameters:

      1. For static properties, the constructor function of the class, for all other properties, the prototype of the class.
      2. The name of the member.

      But differently from the property decorator, it also receives a third parameter, with the Property Descriptor of the accessor member.

      Given the fact that Property Descriptors contains both the setter and getter for a particular member, accessor decorators can only be applied to either the setter or the getter of a single member, not to both.

      If you return a value from your accessor decorator, this value will become the new Property Descriptor of the accessor for both the getter and the setter members.

      Here is an example of a decorator that can be used to change the enumerable flag of a getter/setter accessor:

      const enumerable = (value: boolean) => {
        return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
          propertyDescriptor.enumerable = value;
        }
      }
      

      Notice in the example how you are using a decorator factory. This allows you to specify the enumerable flag when calling your decorator. Here is how you could use your decorator:

      class Person {
        firstName: string = "Jon"
        lastName: string = "Doe"
      
        @enumerable(true)
        get fullName () {
          return `${this.firstName} ${this.lastName}`;
        }
      }
      

      Accessor decorators are similar to property decorators. The only difference is that they receive a third parameter with the property descriptor. Now that you created your first accessor decorator, the next section will show you how to create method decorators.

      Creating Method Decorators

      In this section, you will take a look at how to use method decorators.

      The implementation of method decorators is very similar to the way you create accessor decorators. The parameters passed to the decorator implementation are identical to the ones passed to accessor decorators.

      Let’s re-use that same enumerable decorator you created before, but this time in the getFullName method of the following Person class:

      const enumerable = (value: boolean) => {
        return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
          propertyDescriptor.enumerable = value;
        }
      }
      
      class Person {
        firstName: string = "Jon"
        lastName: string = "Doe"
      
        @enumerable(true)
        getFullName () {
          return `${this.firstName} ${this.lastName}`;
        }
      }
      

      If you returned a value from your method decorator, this value will become the new Property Descriptor of the method.

      Let’s create a deprecated decorator which prints the passed message to the console when the method is used, logging a message saying that the method is deprecated:

      const deprecated = (deprecationReason: string) => {
        return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
          return {
            get() {
              const wrapperFn = (...args: any[]) => {
                console.warn(`Method ${memberName} is deprecated with reason: ${deprecationReason}`);
                propertyDescriptor.value.apply(this, args)
              }
      
              Object.defineProperty(this, memberName, {
                  value: wrapperFn,
                  configurable: true,
                  writable: true
              });
              return wrapperFn;
            }
          }
        }
      }
      

      Here, you are creating a decorator using a decorator factory. This decorator factory receives a single argument of type string, which is the reason for the deprecation, as shown in the highlighted part below:

      const deprecated = (deprecationReason: string) => {
        return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
          // ...
        }
      }
      

      The deprecationReason will be used later when logging the deprecation message to the console. In the implementation of your deprecated decorator, you are returning a value. When you return a value from a method decorator, this value will overwrite this member’s Property Descriptor.

      You are taking advantage of that to add a getter to your decorated class method. This way you can change the implementation of the method itself.

      But why not just use Object.defineProperty instead of returning a new property decorator for the method? This is necessary as you need to access the value of this, which for non-static class methods, is bound to the class instance. If you used Object.defineProperty directly there would be no way for you to retrieve the value of this, and if the method used this in any way, the decorator would break your code when you run the wrapped method from within your decorator implementation.

      In your case, the getter itself has the this value bound to the class instance for non-static methods and bound to the class constructor for static methods.

      Inside your getter you are then creating a wrapper function locally, called wrapperFn, this function logs a message to the console using console.warn, passing the deprecationReason received from the decorator factory, you are then calling the original method, using propertyDescriptor.value.apply(this, args), this way the original method is called with their this value correctly bound to the class instance in case it was a non-static method.

      You are then using defineProperty to overwrite the value of your method in the class. This works like a memoization mechanism, as multiple calls to the same method will not call your getter anymore, but the wrapperFn directly. You are now setting the member in the class to have your wrapperFn as their value by using Object.defineProperty.

      Let’s use your deprecated decorator:

      const deprecated = (deprecationReason: string) => {
        return (target: any, memberName: string, propertyDescriptor: PropertyDescriptor) => {
          return {
            get() {
              const wrapperFn = (...args: any[]) => {
                console.warn(`Method ${memberName} is deprecated with reason: ${deprecationReason}`);
                propertyDescriptor.value.apply(this, args)
              }
      
              Object.defineProperty(this, memberName, {
                  value: wrapperFn,
                  configurable: true,
                  writable: true
              });
              return wrapperFn;
            }
          }
        }
      }
      
      class TestClass {
        static staticMember = true;
      
        instanceMember: string = "hello"
      
        @deprecated("Use another static method")
        static deprecatedMethodStatic() {
          console.log('inside deprecated static method - staticMember=", this.staticMember);
        }
      
        @deprecated("Use another instance method")
        deprecatedMethod () {
          console.log("inside deprecated instance method - instanceMember=", this.instanceMember);
        }
      }
      
      TestClass.deprecatedMethodStatic();
      
      const instance = new TestClass();
      instance.deprecatedMethod();
      

      Here, you created a TestClass with two properties: one static and one non-static. You also created two methods: one static and one non-static.

      You are then applying your deprecated decorator to both methods. When you run the code, the following will appear in the console:

      Output

      (warning) Method deprecatedMethodStatic is deprecated with reason: Use another static method inside deprecated static method - staticMember = true (warning)) Method deprecatedMethod is deprecated with reason: Use another instance method inside deprecated instance method - instanceMember = hello

      This shows that both methods were correctly wrapped with your wrapper function, which logs a message to the console with the deprecation reason.

      You have now created your first method decorator using TypeScript. The next section will show you how to create the last decorator type supported by TypeScript, a parameter decorator.

      Creating Parameter Decorators

      Parameter decorators can be used in class method’s parameters. In this section, you will learn how to create one.

      The decorator function used with parameters receives the following parameters:

      1. For static properties, the constructor function of the class. For all other properties, the prototype of the class.
      2. The name of the member.
      3. The index of the parameter in the method’s parameter list.

      It is not possible to change anything related to the parameter itself, so such decorators are only useful for observing the parameter usage itself (unless you use something more advanced like reflect-metadata).

      Here is an example of a decorator that prints the index of the parameter that was decorated, along with the method name:

      function print(target: Object, propertyKey: string, parameterIndex: number) {
        console.log(`Decorating param ${parameterIndex} from ${propertyKey}`);
      }
      

      Then you can use your parameter decorator like this:

      class TestClass {
        testMethod(param0: any, @print param1: any) {}
      }
      

      Running the above code should display the following in the console:

      Output

      Decorating param 1 from testMethod

      You’ve now created and executed a parameter decorator, and printed out the result that returns the index of the decorated parameter.

      Conclusion

      In this tutorial, you have implemented all decorators supported by TypeScript, used them with classes and learned the differences between each one of them. You can now start writing your own decorators to reduce boilerplate code in your codebase, or use decorators with libraries, such as Mobx, with more confidence.

      For more tutorials on TypeScript, check out our How To Code in TypeScript series page.



      Source link