One place for hosting & domains

      TypeScript

      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

      How To Use Enums in TypeScript


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

      Introduction

      In TypeScript, enums, or enumerated types, are data structures of constant length that hold a set of constant values. Each of these constant values is known as a member of the enum. Enums are useful when setting properties or values that can only be a certain number of possible values. One common example is the suit value of a single card in a deck of playing cards. Every card that is drawn will either be a club, a diamond, a heart, or a spade; there are no possible suit values beyond these four, and these possible values are not likely to change. Because of this, an enum would be an efficient and clear way to describe the possible suits of a card.

      Whereas most features of TypeScript are useful for throwing errors during compilation, enums are also useful as data structures that can hold constants for your code. TypeScript translates enums into JavaScript objects in the final code emitted by the compiler. Because of this, you can use enums to make a codebase more readable, as you can have multiple constant values grouped in the same data structure, while also making the code more type-safe than just having different const variables laying around.

      This tutorial will explain the syntax used to create enum types, the JavaScript code that the TypeScript compiler creates under the hood, how to extract the enum object type, and a use case for enums that involves bit flags in game development.

      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 parameters, and imports/exports. If you need more information on these topics, reading our How To Code in JavaScript series is recommended.
      • This tutorial will reference aspects of text editors that support TypeScript and show in-line errors. This is not necessary to use TypeScript, but does take more advantage of TypeScript features. To gain the benefit of these, you can use a text editor like Visual Studio Code, which has full support for TypeScript out of the box. You can also try out these benefits in the TypeScript Playground.

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

      Creating Enums in TypeScript

      In this section, you will run through an example of declaring both a numeric enum and a string enum.

      Enums in TypeScript are usually used to represent a determined number of options for a given value. This data is arranged in a set of key/value pairs. While the keys must be strings, as with JavaScript objects in general, the values for enum members are often auto-incremented numbers that mainly serve to distinguish one member from the other. Enums with only number values are called numeric enums.

      To create a numeric enum, use the enum keyword, followed by the name of the enum. Then create a curly bracket ({}) block, where you will specify the enum members inside, like this:

      enum CardinalDirection {
        North = 1,
        East,
        South,
        West,
      };
      

      In this example, you are making an enum called CardinalDirection, which has a member that represents each of the cardinal directions. An enum is an appropriate choice of data structure to hold these options, since there are always only four options for values: north, south, east, and west.

      You used the number 1 as the value of the first member of your CardinalDirection enum. This assigns the number 1 to be the value of North. However, you did not assign values to the other members. This is because TypeScript automatically sets the remaining members to the value of the previous member plus one. CardinalDirection.East would have the value 2, CardinalDirection.South would have the value 3, and CardinalDirection.West would have the value 4.

      This behavior only works with numeric enums that have only number values for each member.

      You can also completely ignore setting the value of the enum members:

      enum CardinalDirection {
        North,
        East,
        South,
        West,
      };
      

      In this case, TypeScript is going to set the first member to 0, and then set the other ones automatically based on that one, incrementing each by one. This will result in code identical to the following:

      enum CardinalDirection {
        North = 0,
        East = 1,
        South = 2,
        West = 3,
      };
      

      The TypeScript compiler defaults to assigning numbers to enum members, but you can override this to make a string enum. These are enums that have string values for each member; these are useful when the value needs to carry a certain human-readable meaning, such as if you’ll need to read the value in a log or error message later on.

      You can declare enum members to have string values with the following code:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W"
      }
      

      Now each of the directions has a letter value that indicates which direction they are tied to.

      With the declaration syntax covered, you can now check out the underlying JavaScript to learn more about how enums behave, including the bi-directional nature of the key/value pairs.

      Bi-directional Enum Members

      Upon TypeScript compilation, enums are translated into JavaScript objects. However, there are a few features of enums that differentiate them from objects. They offer a more stable data structure for storing constant members than traditional JavaScript objects, and also offer bi-directional referencing for enum members. To show how this works, this section will show you how TypeScript compiles enums in your final code.

      Take the string enum you created in the last section:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W",
      };
      

      This becomes the following code when compiled to JavaScript using the TypeScript compiler:

      "use strict";
      var CardinalDirection;
      (function (CardinalDirection) {
          CardinalDirection["North"] = "N";
          CardinalDirection["East"] = "E";
          CardinalDirection["South"] = "S";
          CardinalDirection["West"] = "W";
      })(CardinalDirection || (CardinalDirection = {}));
      

      In this code, the "use strict" string starts strict mode, a more restrictive version of JavaScript. After that, TypeScript creates a variable CardinalDirection with no value. The code then contains an immediately invoked function expression (IIFE) that takes the CardinalDirection variable as an argument, while also setting its value to an empty object ({}) if it has not already been set.

      Inside the function, once CardinalDirection is set as an empty object, the code then assigns multiples properties to that object:

      "use strict";
      var CardinalDirection;
      (function (CardinalDirection) {
          CardinalDirection["North"] = "N";
          CardinalDirection["East"] = "E";
          CardinalDirection["South"] = "S";
          CardinalDirection["West"] = "W";
      })(CardinalDirection || (CardinalDirection = {}));
      

      Notice that each property is one member of your original enum, with the values set to the enum’s member value.

      For string enums, this is the end of the process. But next you will try the same thing with the numeric enum from the last section:

      enum CardinalDirection {
        North = 1,
        East,
        South,
        West,
      };
      

      This will result in the following code, with the highlighted sections added:

      "use strict";
      var CardinalDirection;
      (function (CardinalDirection) {
          CardinalDirection[CardinalDirection["North"] = 1] = "North";
          CardinalDirection[CardinalDirection["East"] = 2] = "East";
          CardinalDirection[CardinalDirection["South"] = 3] = "South";
          CardinalDirection[CardinalDirection["West"] = 4] = "West";
      })(CardinalDirection || (CardinalDirection = {}));
      

      In addition to each member of the enum becoming a property of the object (CardinalDirection["North"] = 1]), the enum also creates a key for each number and assigns the string as the value. In the case of North, CardinalDirection["North"] = 1 returns the value 1, and CardinalDirection[1] = "North" assigns the value "North" to the key "1".

      This allows for a bi-directional relationship between the names of the numeric members and their values. To test this out, log the following:

      console.log(CardinalDirection.North)
      

      This will return the value of the "North" key:

      Output

      1

      Next, run the following code to reverse the direction of the reference:

      console.log(CardinalDirection[1])
      

      The output will be:

      Output

      "North"

      To illustrate the final object that represents the enum, log the entire enum to the console:

      console.log(CardinalDirection)
      

      This will show both of the sets of key/value pairs that create the bi-directionality effect:

      Output

      { "1": "North", "2": "East", "3": "South", "4": "West", "North": 1, "East": 2, "South": 3, "West": 4 }

      With an understanding of how enums work under the hood in TypeScript, you will now move on to using enums to declare types in your code.

      Using Enums in TypeScript

      In this section, you will try out the basic syntax of assigning enum members as types in your TypeScript code. This can be done in the same way that basic types are declared.

      To use your CardinalDirection enum as the type of a variable in TypeScript, you can use the enum name, as shown in the following highlighted code:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W",
      };
      
      const direction: CardinalDirection = CardinalDirection.North;
      

      Notice that you are setting the variable to have the enum as its type:

      const direction: CardinalDirection = CardinalDirection.North;
      

      You are also setting the variable value to be one of the members of the enum, in this case CardinalDirection.North. You can do this because enums are compiled to JavaScript objects, so they also have a value representation in addition to being types.

      If you pass a value that is not compatible with the enum type of your direction variable, like this:

      const direction: CardinalDirection = false;
      

      The TypeScript compiler is going to display the error 2322:

      Output

      Type 'false' is not assignable to type 'CardinalDirection'. (2322)

      direction can therefore only be set to a member of the CardinalDirection enum.

      You are also able to set the type of your variable to a specific enum member:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W",
      };
      
      const direction: CardinalDirection.North = CardinalDirection.North;
      

      In this case, the variable can only be assigned to the North member of the CardinalDirection enum.

      If the members of your enum have numeric values, you can also set the value of your variable to those numeric values. For example, given the enum:

      enum CardinalDirection {
        North = 1,
        East,
        South,
        West,
      };
      

      You can set the value of a variable of type CardinalDirection to 1:

      const direction: CardinalDirection = 1;
      

      This is possible because 1 is the value of the North member of your CardinalDirection enum. This only works for numeric members of the enum, and it relies on the bi-directional relationship the compiled JavaScript has for numeric enum members, covered in the last section.

      Now that you have tried out declaring variable types with enum values, the next section will demonstrate a specific way of manipulating enums: extracting the underlying object type.

      In the previous sections, you found that enums are not just a type-level extension on top of JavaScript, but have real values. This also means that the enum data structure itself has a type, which you will have to take into account if you are trying to set a JavaScript object that represents an instance of the enum. To do this, you will need to extract the type of the enum object representation itself.

      Given your CardinalDirection enum:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W",
      };
      

      Try to create an object that matches your enum, like the following:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W",
      };
      
      const test1: CardinalDirection = {
        North: CardinalDirection.North,
        East: CardinalDirection.East,
        South: CardinalDirection.South,
        West: CardinalDirection.West,
      }
      

      In this code, test1 is an object with type CardinalDirection, and the object value includes all the members of the enum. However, the TypeScript compiler is going to show the error 2322:

      Output

      Type '{ North: CardinalDirection; East: CardinalDirection; South: CardinalDirection; West: CardinalDirection; }' is not assignable to type 'CardinalDirection'.

      The reason for this error is that the CardinalDirection type represents a union type of all the enum members, not the type of the enum object itself. You can extract the object type by using typeof before the name of the enum. Check the highlighted code below:

      enum CardinalDirection {
        North="N",
        East="E",
        South="S",
        West="W",
      };
      
      const test1: typeof CardinalDirection = {
        North: CardinalDirection.North,
        East: CardinalDirection.East,
        South: CardinalDirection.South,
        West: CardinalDirection.West,
      }
      

      The TypeScript compiler will now be able to compile your code correctly.

      This section showed a specific way to widen your use of enums. Next, you will work through a use case in which enums are applicable: bit flags in game development.

      Using Bit Flags with TypeScript Enums

      In this last section of the tutorial, you’ll run through a tangible use case for enums in TypeScript: bit flags.

      Bit flags are a way to represent different boolean-like options into a single variable, by using bitwise operations. For this to work, each flag must use exactly one bit of a 32-bit number, as this is the max value allowed by JavaScript when doing bitwise operations. The max 32-bit number is 2,147,483,647, which in binary is 1111111111111111111111111111111, so you have 31 possible flags.

      Imagine you are building a game, and the player may have different skills, like SKILL_A, SKILL_B, and SKILL_C. To make sure your program knows when a player has a certain skill, you can make flags that can be turned on or off, depending on the player’s status.

      With the following pseudocode, give each skill flag a binary value:

      SKILL_A = 0000000000000000000000000000001
      SKILL_B = 0000000000000000000000000000010
      SKILL_C = 0000000000000000000000000000100
      

      You can now store all the current skills of the player in a single variable, by using the bitwise operator | (OR):

      playerSkills = SKILL_A | SKILL_B
      

      In this case, assigning a player the bit flag 0000000000000000000000000000001 and the bit flag 0000000000000000000000000000010 with the | operator will yield 0000000000000000000000000000011, which will represent the player having both skills.

      You are also able to add more skills:

      playerSkills |= SKILL_C
      

      This will yield 0000000000000000000000000000111 to indicate that the player has all three skills.

      You can also remove a skill using a combination of the bitwise operators & (AND) and ~ (NOT):

      playerSkills &= ~SKILL_C
      

      Then to check if the player has a specific skill, you use the bitwise operator & (AND):

      hasSkillC = (playerSkills & SKILL_C) == SKILL_C
      

      If the player does not have the SKILL_C skill, the (playerSkills & SKILL_C) part is going to evaluate to 0. Otherwise (playerSkills & SKILL_C) evaluates to the exact value of the skill you are testing, which in this case is SKILL_C (0000000000000000000000000000010). This way you can test that the evaluated value is the same as the value of the skill you are testing it against.

      As TypeScript allows you to set the value of enum members to integers, you can store those flags as an enum:

      enum PlayerSkills {
        SkillA = 0b0000000000000000000000000000001,
        SkillB = 0b0000000000000000000000000000010,
        SkillC = 0b0000000000000000000000000000100,
        SkillD = 0b0000000000000000000000000001000,
      };
      

      You can use the prefix 0b to represent binary numbers directly. If you do not want to use such big binary representations, you can use the bitwise operator << (left shift):

      enum PlayerSkills {
        SkillA = 1 << 0,
        SkillB = 1 << 1,
        SkillC = 1 << 2,
        SkillD = 1 << 3,
      };
      

      1 << 0 will evaluate to 0b0000000000000000000000000000001, 1 << 1 to 0b0000000000000000000000000000010, 1 << 2 to 0b0000000000000000000000000000100, and 1 << 3 to 0b0000000000000000000000000001000.

      Now you can declare your playerSkills variable like this:

      let playerSkills: PlayerSkills = PlayerSkills.SkillA | PlayerSkills.SkillB;
      

      Note: You must explicitly set the type of the playerSkills variable to be PlayerSkills, otherwise TypeScript will infer it to be of type number.

      To add more skills, you would use the following syntax:

      playerSkills |= PlayerSkills.SkillC;
      

      You can also remove a skill:

      playerSkills &= ~PlayerSkills.SkillC;
      

      Finally, you can check if the player has any given skill using your enum:

      const hasSkillC = (playerSkills & PlayerSkills.SkillC) === PlayerSkills.SkillC;
      

      While still using bit flags under the hood, this solution provides a more readable and organized way to display the data. It also makes your code more type-safe by storing the binary values as constants in an enum, and throwing errors if the playerSkills variable does not match a bit flag.

      Conclusion

      Enums are a common data structure in most languages that provide a type system, and this is no different in TypeScript. In this tutorial, you created and used enums in TypeScript, while also going through a few more advanced scenarios, such as extracting the object type of an enum and using bit flags. With enums, you can make your code base more readable, while also organizing constants into a data dtructure rather than leaving them in the global space.

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



      Source link