One place for hosting & domains

      TypeScript

      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

      How To Build a Bookstore Landing Page with Gatsby and TypeScript


      The author selected the Diversity in Tech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Landing pages are web pages that promote a product or service, providing a place for customers to land when arriving at a site. For businesses, they often are the destination of links in online advertisments and marketing emails. A commercial landing page’s primary goal is to turn visitors into potential clients or customers. Because of this, building a landing page is a valuable skill for a web developer.

      In this tutorial, you will build a landing page with the following two technologies:

      • Gatsby, a React-based frontend framework designed to generate static websites. Gatsby allows you to generate landing pages quickly, which can be useful when creating many landing pages for different projects.

      • TypeScript is a superset of JavaScript that introduces static types and type-checking at build-time. TypeScript has become one of the most widely used alternatives to JavaScript because of its strong typing system, which alerts developers to problems in code before the code gets into proudction. For the landing page, TypeScript will help guard against invalidly typed data for dynamic values, such as the sales pitch text or the signup form input.

      The example landing page you will build in this tutorial will promote a bookstore, and will include the following common components of a landing page:

      • A header for the bookstore
      • A hero image that relates to the store
      • A sales pitch with a list of features/services
      • A email signup form that can be used to add the reader to a mailing list about the business

      The sample project will look like the following image by the end of the tutorial:

      Resulting landing page from following this tutorial, displaying the header, hero image, and sales pitch

      Prerequisites

      Step 1 — Refactoring the Header and Layout Components

      In this step, you will begin by refactoring the existing header.tsx and layout.tsx components of the bookstore-landing-page project that you created in the prerequisite tutorial. This will include replacing default type-definitions with custom-type interfaces and revising some GraphQL queries. Once you have completed this step, you will have populated the header of your landing page with the page’s title and description and created a layout component to implement future components.

      The Header component will display the title and description of your page at the top of the browser window. But before refactoring this component, you will open the gatsby-config.js file in the project’s root directory to update the site’s metadata. Later, you will query gatsby-config.js from the Layout component to retrieve this data.

      Open gatsby-config.js in your text editor of choice. Under siteMetaData in the exported module, change the value of title and description to the name of the bookstore and a business slogan, as shown in the following highlighted code:

      bookstore-landing-page/gatsby-config.js

      module.exports = {
        siteMetadata: {
          title: `The Page Turner`,
          description: `Explore the world through the written word!`,
          author: `@gatsbyjs`,
        },
        plugins: [
          ...
      

      After making these changes, save and close the gatsby-config.js file.

      Next, inside the bookstore-landing-page/src/components directory, open the header.tsx file. From here you will refactor the <Header /> component to use TypeScript typing instead of the default PropTypes. Make the following changes to your code:

      bookstore-landing-page/src/components/header.tsx

      import * as React from "react"
      import { Link } from "gatsby"
      
      interface HeaderProps {
        siteTitle: string,
        description: string
      }
      
      const Header = ({ siteTitle, description }: HeaderProps) => (
        ...
      )
      
      export default Header
      

      You deleted the Header.PropTypes and Header.defaultProps objects after the Header declaration and replaced them with a custom-type interface HeaderProps, using the siteTitle and description properties. Then, you added description to the list of arguments passed to the functional component and assigned them to the HeaderProps type. The newly defined HeaderProps interface will act as a custom type for the arguments passed to the <Header/> component from the GraphQL query in the <Layout/> component.

      Next, in the JSX of the <Header /> component, change the styling in the opening header tag so the background color is blue and the text is center-aligned. Keep siteTitle in the embedded <Link/> component, but add description to a separate <h3/> tag and give it a font color of white:

      bookstore-landing-page/src/components/header.tsx

      ...
      
      const Header = ({ siteTitle, description }: HeaderProps) => (
        <header
          style={{
            background: `#0069ff`,
            textAlign: `center`,
          }}
        >
          <div
            style={{
              margin: `0 auto`,
              maxWidth: 960,
              padding: `1.45rem 1.0875rem`,
            }}
          >
            <h1 style={{ margin: 0 }}>
              <Link
                to="/"
                style={{
                  color: `white`,
                  textDecoration: `none`,
                }}
              >
                {siteTitle}
              </Link>
            </h1>
            <h3 style={{
              color: 'white'
            }}>
              {description}
            </h3>
          </div>
        </header>
      )
      
      export default Header
      

      Now you will have inline styling when data is passed to this component.

      Save the changes in the header.tsx file, then run gatsby develop and go to localhost:8000 on your browser. The page will look like the following:

      Landing page with title rendered in header

      Notice the description has not yet been rendered. In the next step, you will add this to the GraphQL query in layout.tsx to ensure that it is displayed.

      With the <Header/> component ready, you can now refactor the default <Layout/> component for the landing page.

      Layout

      The <Layout /> component will wrap your landing page, and can help share styles and formatting for future pages on your site.

      To start editing this component, open layout.tsx in your text editor. Delete the default type definitions at the end of the file and define a new interface named LayoutProps after the import statements. Then, assign the interface type to the arguments passed to <Layout/>:

      bookstore-landing-page/src/components/layout.tsx

      /**
       * Layout component that queries for data
       * with Gatsby's useStaticQuery component
       *
       * See: https://www.gatsbyjs.com/docs/use-static-query/
       */
      
      import * as React from "react"
      import { useStaticQuery, graphql } from "gatsby"
      
      import Header from "./header"
      import "./layout.css"
      
      interface LayoutProps {
        children: ReactNode
      }
      
      const Layout = ({ children }: LayoutProps) => {
        ...
      }
      
      default export Layout
      

      The interface uses the ReactNode type, which you imported with the React library. This type definition applies to most React child components, which is what <Layout/> renders by default. This will enable you to define a custom-type interface for <Layout/>.

      Next, revise the default GraphQL query located inside the <Layout/> component. Inside of the siteMetaData object, add the description that was set in gatsby-config.js. Then, like with siteTitle, store the fetched value in a new description variable:

      bookstore-landing-page/src/components/layout.tsx

      ...
      
      const Layout = ({ children }: LayoutProps) => {
        const data = useStaticQuery(graphql`
          query SiteTitleQuery {
            site {
              siteMetadata {
                title
                description
              }
            }
          }
        `)
      
        const siteTitle = data.site.siteMetadata?.title || `Title`
        const description = data.site.siteMetadata?.description || 'Description'
      
       ...
      
      

      Now you can pass description as a prop to the <Header/> component in the layout’s returned JSX. This is important because description was defined as a required property in the HeaderProps interface:

      bookstore-landing-page/src/components/layout.tsx

      
      ...
      
        return (
          <>
            <Header siteTitle={siteTitle} description={description}/>
            ...
          </>
        )
      
      export default Layout
      
      

      Save and exit from the layout.tsx file.

      As a final change to your layout, go into layouts.css to make a styling change by centering all text in the body of the page:

      bookstore-landing-page/src/components/layout.css

      ...
      
      /* Custom Styles */
      
      body {
        margin: 0;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        color: hsla(0, 0%, 0%, 0.8);
        font-family: georgia, serif;
        font-weight: normal;
        word-wrap: break-word;
        font-kerning: normal;
        -moz-font-feature-settings: "kern", "liga", "clig", "calt";
        -ms-font-feature-settings: "kern", "liga", "clig", "calt";
        -webkit-font-feature-settings: "kern", "liga", "clig", "calt";
        font-feature-settings: "kern", "liga", "clig", "calt";
        text-align: center;
      }
      
      ...
      

      Save and close the layout.css file, then start the development server and render your site in the browser. You will now find the description value rendered in the header:

      Landing page with rendered header but no content

      Now that you have refactored the base files for your Gatsby site, you can add a hero image to your page to make it more visually appealing to customers.

      Step 2 — Adding a Hero Image

      A hero image is a visual that lends support to the product or service in the landing page. In this step, you will download an image for your bookstore landing page and render it on the site using the <StaticImage /> component of the gatsby-plugin-image plugin.

      Note: This project is using Gatsby version 3.9.0, so you won’t be able to use the deprecated gatsby-image package. This package was replaced with gatsby-plugin-image. This new plugin, along with help from gatsby-plugin-sharp, will render responsive images with processing functionality.

      First, download the bookshelf image from Unsplash, a site that provides images that you can use freely:

      • curl https://images.unsplash.com/photo-1507842217343-583bb7270b66 -o src/images/bookshelf.png

      This command uses curl to download the image. The -o flag designates the output, which you have set to be a file named bookshelf.png in the images directory.

      Now open the src/pages/index.tsx file. The Gatsby default starter template already has a <StaticImage/> component, so replace the attributes to point to your newly downloaded image:

      bookstore-landing-page/src/pages/index.tsx

      import * as React from "react"
      import { StaticImage } from "gatsby-plugin-image"
      
      import Layout from "../components/layout"
      import Seo from "../components/seo"
      
      const IndexPage = () => (
        <Layout>
          <Seo title="Home" />
          <StaticImage
            src="https://www.digitalocean.com/community/tutorials/images/bookshelf.png"
            alt="Bookshelf hero image"
          />
        </Layout>
      )
      
      export default IndexPage
      

      You added a src attribute to direct Gatsby to the correct image in your images directory, then added the alt attribute to provide alternative text for the image.

      Save and close the file, then restart the development server. Your page will now have the downloaded bookshelf image rendered at its center:

      Rendered landing page with bookshelf image.

      With your image now rendered on your site, you can move on to adding some content to the page.

      Step 3 — Creating a Sales Pitch and Features Component

      For the next part of the landing page, you are going to build a new component that holds the sales pitch for your bookstore. This will explain why your customers should come to your store.

      Inside of bookstore-landing-page/src/components, go ahead and create a new file titled salesPitchAndFeatures.tsx. Inside the new file, import React, create a new functional component called SalesPitchAndFeatures, and export it:

      bookstore-landing-page/src/components/salesPitchAndFeatures.tsx

      import * as React from "react"
      
      const SalesPitchAndFeatures = () => {
        <>
        </>
      }
      
      export default SalesPitchAndFeatures
      

      The interface for this component will include an optional salesPitch property of type string. It will also have a list of features of type Array<string>, which is required:

      bookstore-landing-page/src/components/salesPitchAndFeatures.tsx

      import * as React from "react"
      
      interface SalesPitchAndFeaturesProps {
        salesPitch?: string
        features: Array<string>
      }
      ...
      

      The data for the salesPitch and features will be hard-coded within salesPitchAndFeatures.tsx, but you could also store it in another place (like gatsby-config.js) and query the needed data with GraphQL. The content object will be of type SalesPitchAndFeaturesProps:

      bookstore-landing-page/src/components/salesPitchAndFeatures.tsx

      ...
      
      interface salesPitchAndFeaturesProps {
          salesPitch?: string 
          features: Array<string>
      }
      
      const content: SalesPitchAndFeaturesProps = {
          salesPitch: "Come and expand your world at our bookstore! We are always getting new titles for you to see. Everything you need is here at an unbeatable price!",
          features: [ 
          "Tens of thousands of books to browse through",
          "Engage with the community at a book club meeting",
          "From the classics to the newest publications, there's something for everybody!"
      ]}
      
      const SalesPitchAndFeatures = () => {
          return (
              <>
      
                ...
      

      Notice that the salesPitch prop is a string and the features prop is an array of strings, just as you set them in your interface.

      You’ll also need a function that will display the list of features. Create a showFeatures(f)function.

      bookstore-landing-page/src/components/salesPitchAndFeatures.tsx

      ...
      
      const showFeatures: any = (f: string[]) => {
          return f.map(feature => <li>{feature}</li>)
      }
      
      const SalesPitchAndFeatures = () => {
          return (
              <>
      
                ...
      
      

      The argument f passed into showFeatures is of type Array<string> to be consistent with the array of features of type string. To return the list tranformed into rendered JSX, you use the .map() array method.

      Populate the return statement with your content, wrapped in divs with assigned class names for styling:

      bookstore-landing-page/src/components/salesPitchAndFeatures.tsx

      ...
      
      const SalesPitchAndFeatures = () => {
          return (
              <div className="features-container">
                  <p className="features-info">
                      {content.salesPitch}
                  </p>
                  <ul className="features-list">
                      {showFeatures(content.features)}
                  </ul>
              </div>
          )
      }
      
      export default SalesPitchAndFeatures
      

      Save and close salesPitchAndFeatures.tsx.

      Next, open layout.css to add styling to the class names added in the <SalesPitchAndFeatures/> component:

      bookstore-landing-page/src/components/layout.css

      ...
      .features-container {
        border: 1px solid indigo;
        border-radius: 0.25em;
        padding: 2em;
        margin: 1em auto;
      }
      
      .features-list {
        text-align: left;
        margin: auto;
      }
      

      This adds a border around the sales pitch and features list, then adds spacing between the elements to increase readability.

      Save and close layout.css.

      Lastly, you will render this component on the landing page. Open index.tsx in the src/pages/ directory. Add the <SalesPitchAndFeatures/> component to the rendered layout children:

      bookstore-landing-page/src/pages/index.tsx

      import * as React from "react"
      import { StaticImage } from "gatsby-plugin-image"
      
      import Layout from "../components/layout"
      import SalesPitchAndFeatures from "../components/salesPitchAndFeatures"
      import SEO from "../components/seo"
      
      const IndexPage = () => (
        <Layout>
          <SEO title="Home" />
          <div style={{ maxWidth: `450px`, margin: ' 1em auto'}}>
            <StaticImage
              src="https://www.digitalocean.com/community/tutorials/images/bookshelf.png"
              alt="Bookshelf hero image"
            />
            <SalesPitchAndFeatures/>
          </div>
        </Layout>
      )
      
      export default IndexPage
      

      You also added in a div to apply some styling to both the image and the sales pitch.

      Save and exit from the file. Restart your development server and you will find your sales pitch and features list rendered below your image:

      Rendered page with sales pitch and features added

      You now have a sales pitch on your landing page, which will help communicate to potential customers why they should go to your business. Next, you will build the final component for the landing page: an email signup form.

      Step 4 — Creating a Signup Form Component

      An email signup button is a common landing page component that lets the user enter their email address and sign up for more news and information about the product or business. Adding this to your landing page will give the user an actionable step they can take to become your customer.

      To start, create a new file in bookstore-landing-page/src/components called signupForm.tsx. This component won’t have any custom types but will have an event handler, which has its own special React-based type.

      First, build the <SignUpForm/> component and its return statement, with a header inside:

      bookstore-landing-page/src/components/signupForm.tsx

       import * as React from "react"
      
      const SignUpForm = () => {
        return (
          <h3>Sign up for our newsletter!</h3>
        )
      }
      
      export default SignupForm
      

      Next, add some markup to create a form element with an onSubmit attribute, initialized to null for now. Soon this will contain the event handler, but for now, finish writing the form with label, input, and button tags:

      bookstore-landing-page/src/components/signupForm.tsx

      import * as React from "react"
      
      const SignUpForm = () => {
        return (
          <React.Fragment>
            <h3>Sign up for our newsletter!</h3>
            <form onSubmit={null}>
              <div style={{display: 'flex'}}>
                  <input type="email" placeholder="email@here"/>
      
              <button type="submit">Submit</button>
              </div>
            </form>
          <React.Fragment>
        )
      }
      
      export default SignupForm
      

      If you type your email address in the text field and click Sign Up on the rendered landing page right now, nothing will happen. This is because you still need to write an event handler that will trigger whenever the form is submitted. A best practice is to write a separate function outside the return statement for the event handler. This function usually has a special e object that represents the triggered event.

      Before writing the separate function, you will write the function in-line to figure out what the static type of the event object is, so that you can use that type later:

      bookstore-landing-page/src/components/signupForm.tsx

      ...
      
          return (
              <React.Fragment>
                  <h3>Sign up for our newsletter!</h3>
                  <form onSubmit={(e) => null}>
                      <div style={{display: 'flex'}}>
                          <input type="email" placeholder="email@here" style={{width: '100%'}}/>
                          <button type="submit">Submit</button>
                      </div>
                  </form>
              </React.Fragment>
          )
      ...
      }
      
      export default SignupForm
      

      If you are using a text editor like Visual Studio Code, hovering your cursor over the e parameter will use TypeScript’s IntelliSense to show you its expected type, which in this case is React.FormEvent<HTMLFormElement>.

      Now that you know what the expected type of your separate function will be, go ahead and use this to write a new, separate function called handleSubmit:

      bookstore-landing-page/src/components/signupForm.tsx

      import * as React from "react"
      
      const SignupForm = () => {
          const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
              e.preventDefault();
              alert(alert('The submit button was clicked! You're signed up!'))
          }
          return (
              <React.Fragment>
                  <h3>Sign up for our newsletter!</h3>
                  <form onSubmit={handleSubmit}>
                      <div style={{display: 'flex'}}>
                        <input type="email" placeholder="email@here"/>
                        <button type="submit">Submit</button>
                      </div>
                  </form>
              </React.Fragment>
        )
      }
      
      export default SignupForm
      

      The handleSubmit function will now trigger a browser alert when the form is submitted. Note that this is a temporary placeholder; to actually add the user to an email list, you would have to connect this to a back-end database, which is beyond the scope of this tutorial.

      Save and close the signupForm.tsx file.

      Now, open the index.tsx file and add the new <SignupForm/> component:

      bookstore-landing-page/src/pages/index.tsx

      import * as React from "react"
      import { StaticImage } from "gatsby-plugin-image"
      
      import Layout from "../components/layout"
      import SalesPitchAndFeatures from "../components/salesPitchAndFeatures"
      import SignupForm from "../components/signupForm"
      import Seo from "../components/seo"
      
      const IndexPage = () => (
        <Layout>
          <Seo title="Home" />
          <div style={{ maxWidth: `450px`, margin: ' 1em auto'}}>
            <HeroImage />
            <SalesPitchAndFeatures />
            <SignupForm />
          </div>
        </Layout>
      )
      
      export default IndexPage
      

      Save and exit the file.

      Restart your development server, and you will find your completed landing page rendered in your browser, along with the email signup button:

      Email signup button rendered below the feature list on the landing page.

      You have now finished building all the core components of your bookstore landing page.

      Conclusion

      Because Gatsby creates fast static websites and TypeScript allows for data to be statically typed, building a landing page makes for an excellent use case. You can shape the types of its common elements (header, hero image, email signup, etc.) so that incorrect forms of data will trigger errors before they go into production. Gatsby provides the bulk of the structure and styling of the page, allowing you to build on top of it. You can use this knowledge to build other landing pages to promote other products and services quicker and more efficiently.

      If you’d like to learn more about TypeScript, check out our How To Code in TypeScript series, or try our How To Create Static Web Sites with Gatsby.js series to learn more about Gatsby.



      Source link

      How To Use Classes in TypeScript


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

      Introduction

      Classes are a common abstraction used in object-oriented programming (OOP) languages to describe data structures known as objects. These objects may contain an initial state and implement behaviors bound to that particular object instance. In 2015, ECMAScript 6 introduced a new syntax to JavaScript to create classes that internally uses the prototype features of the language. TypeScript has full support for that syntax and also adds features on top of it, like member visibility, abstract classes, generic classes, arrow function methods, and a few others.

      This tutorial will go through the syntax used to create classes, the different features available, and how classes are treated in TypeScript during the compile-time type-check. It will lead you through examples with different code samples, which you can follow along with in your own TypeScript environment.

      Prerequisites

      To follow this tutorial:

      • 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.3.2.

      Creating Classes in TypeScript

      In this section, you will run through examples of the syntax used to create classes in TypeScript. While you will cover some of the fundamental aspects of creating classes with TypeScript, the syntax is mostly the same used to create classes with JavaScript. Because of this, this tutorial will focus on some of the distinguishing features available in TypeScript.

      You can create a class declaration by using the class keyword, followed by the class name and then a {} pair block, as shown in the following code:

      class Person {
      
      }
      

      This snippet creates a new class named Person. You can then create a new instance of the Person class by using the new keyword followed by the name of your class and then an empty parameter list (which may be omitted), as shown in the following highlighted code:

      class Person {
      
      }
      
      const personInstance = new Person();
      

      You can think of the class itself as a blueprint for creating objects with the given shape, while instances are the objects themselves, created from this blueprint.

      When working with classes, most of the time you will need to create a constructor function. A constructor is a method that runs every time a new instance of the class is created. This can be used to initialize values in the class.

      Introduce a constructor to your Person class:

      class Person {
        constructor() {
          console.log("Constructor called");
        }
      }
      
      const personInstance = new Person();
      

      This constructor will log Constructor called to the console when personInstance is created.

      Constructors are similar to normal functions in the way that they accept parameters. Those parameters are passed to the constructor when you create a new instance of your class. Currently, you are not passing any parameters to the constructor, as shown by the empty parameter list () when creating the instance of your class.

      Next, introduce a new parameter called name of type string:

      class Person {
        constructor(name: string) {
          console.log(`Constructor called with name=${name}`);
        }
      }
      
      const personInstance = new Person("Jane");
      

      In the highlighted code, you added a parameter called name of type string to your class constructor. Then, when creating a new instance of the Person class, you are also setting the value of that parameter, in this case to the string "Jane". Finally, you changed the console.log to print the argument to the screen.

      If you were to run this code, you would receive the following output in the terminal:

      Output

      Constructor called with name=Jane

      The parameter in the constructor is not optional here. This means that when you instantiate the class, you must pass the name parameter to the constructor. If you do not pass the name parameter to the constructor, like in the following example:

      const unknownPerson = new Person;
      

      The TypeScript Compiler will give the error 2554:

      Output

      Expected 1 arguments, but got 0. (2554) filename.ts(4, 15): An argument for 'name' was not provided.

      Now that you have declared a class in TypeScript, you will move on to manipulating those classes by adding properties.

      Adding Class Properties

      One of the most useful aspects of classes is their ability to hold data that is internal to each instance created from the class. This is done using properties.

      TypeScript has a few safety checks that differentiate this process from JavaScript classes, including a requirement to initialize properties to avoid them being undefined. In this section, you will add new properties to your class to illustrate these safety checks.

      With TypeScript, you usually have to declare the property first in the body of the class and give it a type. For example, add a name property to your Person class:

      class Person {
        name: string;
      
        constructor(name: string) {
          this.name = name;
        }
      }
      

      In this example, you declare the property name with type string in addition to setting the property in the constructor.

      Note: In TypeScript, you can also declare the visibility of properties in a class to determine where the data can be accessed. In the name: string declaration, the visibility is not declared, which means that the property uses the default public status that is accessible anywhere. If you wanted to control the visibility explicitly, you would put declare this with the property. This will be covered more in depth later in the tutorial.

      You are also able to give a default value to a property. As an example, add a new property called instantiatedAt that will be set to the time the class instance was instantiated:

      class Person {
        name: string;
        instantiatedAt = new Date();
      
        constructor(name: string) {
          this.name = name;
        }
      }
      

      This uses the Date object to set an initial date for the creation of the instance. This code works because the code for the default value is executed when the class constructor is called, which would be equivalent to setting the value on the constructor, as shown in the following:

      class Person {
        name: string;
        instantiatedAt: Date;
      
        constructor(name: string) {
          this.name = name;
          this.instantiatedAt = new Date();
        }
      }
      

      By declaring the default value in the body of the class, you do not need to set the value in the constructor.

      Note that if you set a type for a property in a class, you must also initialize that property to a value of that type. To illustrate this, declare a class property but do not provide an initializer to it, like in the following code:

      class Person {
        name: string;
        instantiatedAt: Date;
      
        constructor(name: string) {
          this.name = name;
        }
      }
      

      instantiatedAt is assigned a type of Date, so must always be a Date object. But since there is no initialization, the property becomes undefined when the class is instantiated. Because of this, the TypeScript Compiler is going to show the error 2564:

      Output

      Property 'instantiatedAt' has no initializer and is not definitely assigned in the constructor. (2564)

      This is an additional TypeScript safety check to ensure that the correct properties are present upon class instantiation.

      TypeScript also has a shortcut for writing properties that have the same name as the parameters passed to the constructor. This shortcut is called parameter properties.

      In the previous example, you set the name property to the value of the name parameter passed to the class constructor. This may become tiresome to write if you add more fields to your class. For example, add a new field called age of type number to your Person class and also add it to the constructor:

      class Person {
        name: string;
        age: number;
        instantiatedAt = new Date();
      
        constructor(name: string, age: number) {
          this.name = name;
          this.age = age;
        }
      }
      

      While this works, TypeScript can reduce such boilerplate code with parameter properties, or properties set in the parameters for the constructor:

      class Person {
        instantiatedAt = new Date();
      
        constructor(
          public name: string,
          public age: number
        ) {}
      }
      

      In this snippet, you removed the name and age property declarations from the class body and moved them to be inside the parameters list of the constructor. When you do that, you are telling TypeScript that those constructor parameters are also properties of that class. This way you do not need to set the property of the class to the value of the parameter received in the constructor, as you did before.

      Note: Notice the visibility modifier public has been explicitly stated in the code. This modifier must be included when setting parameter properties, and will not automatically default to public visibility.

      If you take a look at the compiled JavaScript emitted by the TypeScript Compiler, this code compiles to the following JavaScript code:

      "use strict";
      class Person {
        constructor(name, age) {
          this.name = name;
          this.age = age;
          this.instantiatedAt = new Date();
        }
      }
      

      This is the same JavaScript code that the original example compiles to.

      Now that you have tried out setting properties on TypeScript classes, you can move on to extending classes into new classes with class inheritance.

      Class Inheritance in TypeScript

      TypeScript offers the full capability of JavaScript’s class inheritance, with two main additions: interfaces and abstract classes. An interface is a structure that describes and enforces the shape of a class or an object, like providing type-checking for more complex pieces of data. You can implement an interface in a class to make sure that it has a specific public shape. Abstract classes are classes that serve as the basis for other classes, but cannot be instantiated themselves. Both of these are implemented via class inheritance.

      In this section, you will run through some examples of how interfaces and abstract classes can be used to build and create type checks for classes.

      Implementing Interfaces

      Interfaces are useful to specify a set of behaviors that all implementations of that interface must possess. Interfaces are created by using the interface keyword followed by the name of the interface, and then the interface body. As an example, create a Logger interface that could be used to log important data about how your program is running:

      interface Logger {}
      

      Next, add four methods to your interface:

      interface Logger {
        debug(message: string, metadata?: Record<string, unknown>): void;
        info(message: string, metadata?: Record<string, unknown>): void;
        warning(message: string, metadata?: Record<string, unknown>): void;
        error(message: string, metadata?: Record<string, unknown>): void;
      }
      

      As shown in this code block, when creating the methods in your interface, you do not add any implementation to them, just their type information. In this case, you have four methods: debug, info, warning, and error. All of them share the same type signature: They receive two parameters, a message of type string and an optional metadata parameter of type Record<string, unknown>. They all return the void type.

      All classes implementing this interface must have the corresponding parameters and return types for each of these methods. Implement the interface in a class named ConsoleLogger, which logs all messages using console methods:

      class ConsoleLogger implements Logger {
        debug(message: string, metadata?: Record<string, unknown>) {
          console.info(`[DEBUG] ${message}`, metadata);
        }
        info(message: string, metadata?: Record<string, unknown>) {
          console.info(message, metadata);
        }
        warning(message: string, metadata?: Record<string, unknown>) {
          console.warn(message, metadata);
        }
        error(message: string, metadata?: Record<string, unknown>) {
          console.error(message, metadata);
        }
      }
      

      Notice that when creating your interface, you are using a new keyword called implements to specify the list of interfaces your class implements. You can implement multiple interfaces by adding them as a comma-separated list of interface identifiers after the implements keyword. For example, if you had another interface called Clearable:

      interface Clearable {
        clear(): void;
      }
      

      You could implement it in the ConsoleLogger class by adding the following highlighted code:

      class ConsoleLogger implements Logger, Clearable {
        clear() {
          console.clear();
        }
        debug(message: string, metadata?: Record<string, unknown>) {
          console.info(`[DEBUG] ${message}`, metadata);
        }
        info(message: string, metadata?: Record<string, unknown>) {
          console.info(message, metadata);
        }
        warning(message: string, metadata?: Record<string, unknown>) {
          console.warn(message, metadata);
        }
        error(message: string, metadata?: Record<string, unknown>) {
          console.error(message, metadata);
        }
      }
      

      Notice that you also have to add the clear method to make sure the class adheres to the new interface.

      If you did not provide the implementation for one of the members required by any of the interfaces, like the debug method from the Logger interface, the TypeScript compiler would give you the error 2420:

      Output

      Class 'ConsoleLogger' incorrectly implements interface 'Logger'. Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)

      The TypeScript Compiler would also show an error if your implementation did not match the one expected by the interface you are implementing. For example, if you changed the type of the message parameter in the debug method from string to number, you would receive error 2416:

      Output

      Property 'debug' in type 'ConsoleLogger' is not assignable to the same property in base type 'Logger'. Type '(message: number, metadata?: Record<string, unknown> | undefined) => void' is not assignable to type '(message: string, metadata: Record<string, unknown>) => void'. Types of parameters 'message' and 'message' are incompatible. Type 'string' is not assignable to type 'number'. (2416)

      Building on Abstract Classes

      Abstract classes are similar to normal classes, with two major differences: They cannot be directly instantiated and they may contain abstract members. Abstract members are members that must be implemented in inheriting classes. They do not have an implementation in the abstract class itself. This is useful because you can have some common functionality in the base abstract class, and more specific implementations in the inheriting classes. When you mark a class as abstract, you are saying that this class has missing functionality that should be implemented in inheriting classes.

      To create an abstract class, you add the abstract keyword before the class keyword, like in the highlighted code:

      abstract class AbstractClassName {
      
      }
      

      Next, you can create members in your abstract class, some that may have an implementation and others that will not. Ones without implementation are marked as abstract and must then be implemented in the classes that extend from your abstract class.

      For example, imagine you are working in a Node.js environment and you are creating your own Stream implementation. For that, you are going to have an abstract class called Stream with two abstract methods, read and write:

      declare class Buffer {
        from(array: any[]): Buffer;
        copy(target: Buffer, offset?: number): void;
      }
      
      abstract class Stream {
      
        abstract read(count: number): Buffer;
      
        abstract write(data: Buffer): void;
      }
      

      The Buffer object here is a class available in Node.js that is used to store binary data. The declare class Buffer statement at the top allows the code to compile in a TypeScript environment without the Node.js type declarations, like TypeScript Playground.

      In this example, the read method counts bytes from the internal data structure and returns a Buffer object, and write writes all the contents of the Buffer instance to the stream. Both of these methods are abstract, and can only be implemented in classes extended from Stream.

      You can then create additional methods that do have an implementation. This way any class extending from your Stream abstract class would receive those methods automatically. One such example would be a copy method:

      declare class Buffer {
        from(array: any[]): Buffer;
        copy(target: Buffer, offset?: number): void;
      }
      
      abstract class Stream {
      
        abstract read(count: number): Buffer;
      
        abstract write(data: Buffer): void;
      
        copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
          const data = this.read(count);
          data.copy(targetBuffer, targetBufferOffset);
        }
      }
      

      This copy method copies the result from reading the bytes from the stream to the targetBuffer, starting at targetBufferOffset.

      If you then create an implementation for your Stream abstract class, like a FileStream class, the copy method would be readily available, without having to duplicate it in your FileStream class:

      declare class Buffer {
        from(array: any[]): Buffer;
        copy(target: Buffer, offset?: number): void;
      }
      
      abstract class Stream {
      
        abstract read(count: number): Buffer;
      
        abstract write(data: Buffer): void;
      
        copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
          const data = this.read(count);
          data.copy(targetBuffer, targetBufferOffset);
        }
      }
      
      class FileStream extends Stream {
        read(count: number): Buffer {
          // implementation here
          return new Buffer();
        }
      
        write(data: Buffer) {
          // implementation here
        }
      }
      
      const fileStream = new FileStream();
      

      In this example, the fileStream instance automatically has the copy method available on it. The FileStream class also had to implement a read and a write method explicitly to adhere to the Stream abstract class.

      If you had forgotten to implement one of the abstract members of the abstract class you are extending from, like not adding the write implementation in your FileStream class, the TypeScript compiler would give error 2515:

      Output

      Non-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)

      The TypeScript compiler would also display an error if you implemented any of the members incorrectly, like changing the type of the first parameter of the write method to be of type string instead of Buffer:

      Output

      Property 'write' in type 'FileStream' is not assignable to the same property in base type 'Stream'. Type '(data: string) => void' is not assignable to type '(data: Buffer) => void'. Types of parameters 'data' and 'data' are incompatible. Type 'Buffer' is not assignable to type 'string'. (2416)

      With abstract classes and interfaces, you are able to put together more complex type-checking for your classes to ensure that classes extended from base classes inherit the correct functionality. Next, you will run through examples of how method and property visibility work in TypeScript.

      Class Members Visibility

      TypeScript augments the available JavaScript class syntax by allowing you to specify the visibility of the members of a class. In this case, visibility refers to how code outside of an instantiated class can interact with a member inside the class.

      Class members in TypeScript may have three possible visibility modifiers: public, protected, and private. public members may be accessed outside of the class instance, where as private ones cannot. protected occupies a middle ground between the two, where members can be accessed by instances of the class or subclasses based on that class.

      In this section, you are going to examine the available visibility modifiers and learn what they mean.

      public

      This is the default visibility of class members in TypeScript. When you do not add the visibility modifier to a class member, it is the same as setting it to public. Public class members may be accessed anywhere, without any restrictions.

      To illustrate this, return to your Person class from earlier:

      class Person {
        public instantiatedAt = new Date();
      
        constructor(
          name: string,
          age: number
        ) {}
      }
      

      This tutorial mentioned that the two properties name and age had public visibility by default. To declare type visibility explicitly, add the public keyword before the properties and a new public method to your class called getBirthYear, which retrieves the year of birth for the Person instance:

      class Person {
        constructor(
          public name: string,
          public age: number
        ) {}
      
        public getBirthYear() {
          return new Date().getFullYear() - this.age;
        }
      }
      

      You can then use the properties and methods in the global space, outside the class instance:

      class Person {
        constructor(
          public name: string,
          public age: number
        ) {}
      
        public getBirthYear() {
          return new Date().getFullYear() - this.age;
        }
      }
      
      const jon = new Person("Jon", 35);
      
      console.log(jon.name);
      console.log(jon.age);
      console.log(jon.getBirthYear());
      

      This code would print the following to the console:

      Output

      Jon 35 1986

      Notice that you can access all the members of your class.

      protected

      Class members with the protected visibility are only allowed to be used inside the class they are declared in or in the subclasses of that class.

      Take a look at the following Employee class and the FinanceEmployee class that is based on it:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      

      The highlighted code shows the identifier property declared with protected visibility. The this.identifier code tries to access this property from the FinanceEmployee subclass. This code would run without error in TypeScript.

      If you tried to use that method from a place that is not inside the class itself, or inside a subclass, like in the following example:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      
      const financeEmployee = new FinanceEmployee('abc-12345');
      financeEmployee.identifier;
      

      The TypeScript compiler would give us the error 2445:

      Output

      Property 'identifier' is protected and only accessible within class 'Employee' and its subclasses. (2445)

      This is because the identifier property of the new financeEmployee instance cannot be retrieved from the global space. Instead, you would have to use the internal method getFinanceIdentifier to return a string that included the identifier property:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      
      const financeEmployee = new FinanceEmployee('abc-12345');
      console.log(financeEmployee.getFinanceIdentifier())
      

      This would log the following to the console:

      Output

      fin-abc-12345

      private

      Private members are only accessible inside the class that declares them. This means that not even subclasses have access to it.

      Using the previous example, turn the identifier property in the Employee class into a private property:

      class Employee {
        constructor(
          private identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      

      This code will now cause the TypeScript compiler to show the error 2341:

      Output

      Property 'identifier' is private and only accessible within class 'Employee'. (2341)

      This happens because you are accessing the property identifier in the FinanceEmployee subclass, and this is not allowed, as the identifier property was declared in the Employee class and has its visibility set to private.

      Remember that TypeScript is compiled to raw JavaScript that by itself does not have any way to specify the visibility of the members of a class. As such, TypeScript has no protection against such usage during runtime. This is a safety check done by the TypeScript compiler only during compilation.

      Now that you’ve tried out visibility modifiers, you can move on to arrow functions as methods in TypeScript classes.

      Class Methods as Arrow Functions

      In JavaScript, the this value that represents a function’s context can change depending on how a function is called. This variability can sometimes be confusing in complex pieces of code. When working with TypeScript, you can use a special syntax when creating class methods to avoid this being bound to something else other than the class instance. In this section, you will try out this syntax.

      Using your Employee class, introduce a new method used only to retrieve the employee identifier:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      }
      

      This works pretty well if you call the method directly:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      }
      
      const employee = new Employee("abc-123");
      
      console.log(employee.getIdentifier());
      

      This would print the following to the console’s output:

      Output

      abc-123

      However, if you stored the getIdentifier instance method somewhere for it to be called later, like in the following code:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      }
      
      const employee = new Employee("abc-123");
      
      const obj = {
        getId: employee.getIdentifier
      }
      
      console.log(obj.getId());
      

      The value would be inaccessible:

      Output

      undefined

      This happens because when you call obj.getId(), the this inside employee.getIdentifier is now bound to the obj object, and not to the Employee instance.

      You can avoid this by changing your getIdentifier to be an arrow function. Check the highlighted change in the following code:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier = () => {
          return this.identifier;
        }
      }
      ...
      

      If you now try to call obj.getId() like you did before, the console correctly shows:

      Output

      abc-123

      This demonstrates how TypeScript allows you to use arrow functions as direct values of class methods. In the next section, you will learn how to enforce classes with TypeScript’s type-checking.

      Using Classes as Types

      So far this tutorial has covered how to create classes and use them directly. In this section, you will use classes as types when working with TypeScript.

      Classes are both a type and a value in TypeScript, and as such, can be used both ways. To use a class as a type, you use the class name in any place that TypeScript expects a type. For example, given the Employee class you created previously:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      

      Imagine you wanted to create a function that prints the identifier of any employee. You could create such a function like this:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      function printEmployeeIdentifier(employee: Employee) {
        console.log(employee.identifier);
      }
      

      Notice that you are setting the employee parameter to be of type Employee, which is the exact name of your class.

      Classes in TypeScript are compared against other types, including other classes, just like other types are compared in TypeScript: structurally. This means that if you had two different classes that both had the same shape (that is, the same set of members with the same visibility), both can be used interchangeably in places that would expect only one of them.

      To illustrate this, imagine you have another class in your application called Warehouse:

      class Warehouse {
        constructor(
          public identifier: string
        ) {}
      }
      

      It has the same shape as Employee. If you tried to pass an instance of it to printEmployeeIdentifier:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      class Warehouse {
        constructor(
          public identifier: string
        ) {}
      }
      
      function printEmployeeIdentifier(employee: Employee) {
        console.log(employee.identifier);
      }
      
      const warehouse = new Warehouse("abc");
      
      printEmployeeIdentifier(warehouse);
      

      The TypeScript compiler would not complain. You could even use just a normal object instead of the instance of a class. As this may result in a behavior that is not expected by a programmer that is just starting with TypeScript, it is important to keep an eye on these scenarios.

      With the basics of using a class as a type out of the way, you can now learn how to check for specific classes, rather than just the shape.

      The Type of this

      Sometimes you will need to reference the type of the current class inside some methods in the class itself. In this section, you will find out how to use this to accomplish this.

      Imagine you had to add a new method to your Employee class called isSameEmployeeAs, which would be responsible for checking if another employee instance references the same employee as the current one. One way you could do this would be like the following:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      
        isSameEmployeeAs(employee: Employee) {
          return this.identifier === employee.identifier;
        }
      }
      

      This test will work to compare the identifier property of all classes derived from Employee. But imagine a scenario in which you do not want specific subclasses of Employee to be compared at all. In this case, instead of receiving the boolean value of the comparison, you would want TypeScript to report an error when two different subclasses are compared.

      For example, create two new subclasses for employees in the finance and marketing departments:

      ...
      class FinanceEmployee extends Employee {
        specialFieldToFinanceEmployee="";
      }
      
      class MarketingEmployee extends Employee {
        specialFieldToMarketingEmployee="";
      }
      
      const finance = new FinanceEmployee("fin-123");
      const marketing = new MarketingEmployee("mkt-123");
      
      marketing.isSameEmployeeAs(finance);
      

      Here you derive two classes from the Employee base class: FinanceEmployee and MarketingEmployee. Each one has different new fields. You are then creating one instance of each one, and checking if the marketing employee is the same as the finance employee. Given this scenario, TypeScript should report an error, since subclasses should not be compared at all. This does not happen because you used Employee as the type of the employee parameter in your isSameEmployeeAs method, and all classes derived from Employee will pass the type-checking.

      To improve this code, you could use a special type available inside classes, which is the this type. This type is dynamically set to the type of the current class. This way, when this method is called in a derived class, this is set to the type of the derived class.

      Change your code to use this instead:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      
        isSameEmployeeAs(employee: this) {
          return this.identifier === employee.identifier;
        }
      }
      
      class FinanceEmployee extends Employee {
        specialFieldToFinanceEmployee="";
      }
      
      class MarketingEmployee extends Employee {
        specialFieldToMarketingEmployee="";
      }
      
      const finance = new FinanceEmployee("fin-123");
      const marketing = new MarketingEmployee("mkt-123");
      
      marketing.isSameEmployeeAs(finance);
      

      When compiling this code, the TypeScript compiler will now show the error 2345:

      Output

      Argument of type 'FinanceEmployee' is not assignable to parameter of type 'MarketingEmployee'. Property 'specialFieldToMarketingEmployee' is missing in type 'FinanceEmployee' but required in type 'MarketingEmployee'. (2345)

      With the this keyword, you can change typing dynamically in different class contexts. Next, you will use typing for passing in a class itself, rather than an instance of a class.

      Using Construct Signatures

      There are times when a programmer needs to create a function that takes a class directly, instead of an instance. For that, you need to use a special type with a construct signature. In this section, you will go through how to create such types.

      One particular scenario in which you may need to pass in a class itself is a class factory, or a function that generates new instances of classes that are passed in as arguments. Imagine you want to create a function that takes a class based on Employee, creates a new instance with an incremented identifier, and prints the identifier to the console. One may try to create this like the following:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      let identifier = 0;
      function createEmployee(ctor: Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      

      In this snippet, you create the Employee class, initialize the identifier, and create a function that instantiates a class based on a constructor parameter ctor that has the shape of Employee. But if you tried to compile this code, the TypeScript compiler would give the error 2351:

      Output

      This expression is not constructable. Type 'Employee' has no construct signatures. (2351)

      This happens because when you use the name of your class as the type for ctor, the type is only valid for instances of the class. To get the type of the class constructor itself, you have to use typeof ClassName. Check the following highlighted code with the change:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      let identifier = 0;
      function createEmployee(ctor: typeof Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      

      Now your code will compile successfully. But there is still a pending issue: Since class factories build instances of new classes built from a base class, using abstract classes could improve the workflow. However, this will not work initially.

      To try this out, turn the Employee class into an abstract class:

      abstract class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      let identifier = 0;
      function createEmployee(ctor: typeof Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      

      The TypeScript compiler will now give the error 2511:

      Output

      Cannot create an instance of an abstract class. (2511)

      This error shows that you cannot create an instance from the Employee class, since it is abstract. But you may want to use such a function to create different kinds of employees that extend from your Employee abstract class, like such:

      abstract class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {}
      
      class MarketingEmployee extends Employee {}
      
      let identifier = 0;
      function createEmployee(ctor: typeof Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      
      createEmployee(FinanceEmployee);
      createEmployee(MarketingEmployee);
      

      To make your code work for this scenario, you have to use a type with a constructor signature. You can do this by using the new keyword, followed by a syntax similar to that of an arrow function, where the parameter list contains the parameters expected by the constructor and the return type is the class instance this constructor returns.

      Highlighted in the following code is the change introducing the type with a constructor signature to your createEmployee function:

      abstract class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {}
      
      class MarketingEmployee extends Employee {}
      
      let identifier = 0;
      function createEmployee(ctor: new (identifier: string) => Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      
      createEmployee(FinanceEmployee);
      createEmployee(MarketingEmployee);
      

      The TypeScript compiler now will correctly compile your code.

      Conclusion

      Classes in TypeScript are even more powerful than they are in JavaScript because you have access to the type system, extra syntax like arrow function methods, and completely new features like member visibility and abstract classes. This offers a way for you to deliver code that is type-safe, more reliable, and that better represents the business model of your application.

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



      Source link