One place for hosting & domains

      Create

      How To Create and Use Templates in Ansible Playbooks



      Part of the Series:
      How To Write Ansible Playbooks

      Ansible is a modern configuration management tool that doesn’t require the use of an agent software on remote nodes, using only SSH and Python to communicate and execute commands on managed servers. This series will walk you through the main Ansible features that you can use to write playbooks for server automation. At the end, we’ll see a practical example of how to create a playbook to automate setting up a remote Nginx web server and deploy a static HTML website to it.

      Templates allow you to create new files on the nodes using predefined models based on the Jinja2 templating system. Ansible templates are typically saved as .tpl files and support the use of variables, loops, and conditional expressions.

      Templates are commonly used to configure services based on variable values that can be set up on the playbook itself, in included variable files, or obtained via facts. This enables you to create more versatile setups that adapt behavior based on dynamic information.

      To try it out this feature with a practical example, create a new directory to hold non-playbook files inside your ansible-practice directory:

      • mkdir ~/ansible-practice/files

      Next, create a new template file for an HTML landing page. Later on, we’ll set up a playbook which will configure your remote nodes to serve the landing page with Nginx:

      • nano ~/ansible-practice/files/landing-page.html.j2

      Add the following content to the template file:

      ~/ansible-practice/files/landing-page.html.j2

      <!doctype html>
      <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>{{ page_title }}</title>
        <meta name="description" content="Created with Ansible">
      </head>
      <body>
          <h1>{{ page_title }}</h1>
          <p>{{ page_description }}</p>
      </body>
      </html>
      

      Save and close the file when you’re done.

      This template uses two variables that must be provided whenever the template is applied in a playbook: page_title and page_description.

      The following playbook sets up the required variables, installs Nginx, and then applies the specified template to replace the existing, default Nginx landing page located at /var/www/html/index.nginx-debian.html. The last task uses the ufw module to enable tcp access on port 80, in case you have your firewall enabled as recommended in our initial server setup guide.

      Create a new file called playbook-11.yml in your ansible-practice directory:

      • nano ~/ansible-practice/playbook-11.yml

      Add the following content to the new playbook file:

      ~/ansible-practice/playbook-11.yml

      ---
      - hosts: all
        become: yes
        vars:
          page_title: My Landing Page
          page_description: This is my landing page description.
        tasks:
          - name: Install Nginx
            apt:
              name: nginx
              state: latest
      
          - name: Apply Page Template
            template:
              src: files/landing-page.html.j2
              dest: /var/www/html/index.nginx-debian.html
      
          - name: Allow all access to tcp port 80
            ufw:
              rule: allow
              port: '80'
              proto: tcp    
      

      Remember to provide the -K option if you run this playbook, since it requires sudo permissions:

      • ansible-playbook -i inventory playbook-11.yml -u sammy -K

      Output

      BECOME password: PLAY [all] ********************************************************************************************** TASK [Gathering Facts] ********************************************************************************** ok: [203.0.113.10] TASK [Install Nginx] ************************************************************************************ changed: [203.0.113.10] TASK [Apply Page Template] ****************************************************************************** changed: [203.0.113.10] TASK [Allow all access to tcp port 80] ****************************************************************** changed: [203.0.113.10] PLAY RECAP ********************************************************************************************** 203.0.113.10 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

      When the play has finished, you can access the web server’s public IP address from your browser. You’ll see a page like this:

      Screenshot showing custom landing page

      That means your playbook worked as expected, and the default Nginx page was replaced by the template you have created.



      Source link

      How To Create Reusable Blocks of Code with Vue Single-File Components


      The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

      Introduction

      When creating a web application using Vue.js, it’s a best practice to construct your application in small, modular blocks of code. Not only does this keep the parts of your application focused, but it also makes the application easier to update as it grows in complexity. Since an app generated from the Vue CLI requires a build step, you have access to a Single-File Components (SFC) to introduce modularity into your app. SFCs have the .vue extension and contain an HTML <template>, <script>, and <style> tags and can be implemented in other components.

      SFCs give the developer a way to create their own HTML tags for each of their components and then use them in their application. In the same way that the <p> HTML tag will render a paragraph in the browser, and hold non-rendered functionality as well, the component tags will render the SFC wherever it is placed in the Vue template.

      In this tutorial, you are going to create a SFC and use props to pass data down and slots to inject content between tags. By the end of this tutorial, you will have a general understanding of what SFCs are and how to approach code re-usability.

      Prerequisites

      Step 1 — Setting Up the Project

      In this tutorial, you are going to be creating an airport card component that displays a number of airports and their codes in a series of cards. After following the Prerequisites section, you will have a new Vue project named sfc-project. In this section, you will import data into this generated application. This data will be an array of objects consisting of a few properties that you will use to display information in the browser.

      Once the project is generated, open your terminal and cd or change directory into the root src folder:

      From there, create a new directory named data with the mkdir command, then create a new file with the name us-airports.js using the touch command:

      • mkdir data
      • touch data/us-airports.js

      In your text editor of choice, open this new JavaScript file and add in the following local data:

      sfc-project/data/us-airports.js

      export default [
        {
          name: 'Cincinnati/Northern Kentucky International Airport',
          abbreviation: 'CVG',
          city: 'Hebron',
          state: 'KY'
        },
        {
          name: 'Seattle-Tacoma International Airport',
          abbreviation: 'SEA',
          city: 'Seattle',
          state: 'WA'
        },
        {
          name: 'Minneapolis-Saint Paul International Airport',
          abbreviation: 'MSP',
          city: 'Bloomington',
          state: 'MN'
        }
      ]
      

      This data is an array of objects consisting of a few airports in the United States. This will be rendered in a Single-File Component later in the tutorial.

      Save and exit the file.

      Next, you will create another set of airport data. This data will consist of European airports. Using the touch command, create a new JavaScript file named eu-airports.js:

      • touch data/eu-airports.js

      Then open the file and add the following data:

      sfc-project/data/eu-airports.js

      export default [
        {
          name: 'Paris-Charles de Gaulle Airport',
          abbreviation: 'CDG',
          city: 'Paris',
          state: 'Ile de France'
        },
        {
          name: 'Flughafen München',
          abbreviation: 'MUC',
          city: 'Munich',
          state: 'Bavaria'
        },
        {
          name: 'Fiumicino "Leonardo da Vinci" International Airport',
          abbreviation: 'FCO',
          city: 'Rome',
          state: 'Lazio'
        }
      ]
      

      This set of data is for European airports in France, Germany, and Italy, respectively.

      Save and exit the file.

      Next, in the root directory, run the following command in your terminal to start your Vue CLI application running on a local development server:

      This will open the application in your browser on localhost:8080. The port number may be different on your machine.

      Visit the address in your browser. You will find the following start up screen:

      Vue default template page

      Next, start a new terminal and open your App.vue file in your src folder. In this file, delete the img and HelloWorld tags in the <template> and the components section and import statement in the <script>. Your App.vue will resemble the following:

      sfc-project/src/App.vue

      <template>
      
      </template>
      
      <script>
      export default {
        name: 'App',
      }
      </script>
      
      <style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      </style>
      

      After this, import the us-airports.js file that you created earlier. In order to make this data reactive so you can use it in the <template>, you need to import the ref function from vue. You will need to return the airport reference so the data can be used in the HTML template.

      Add the following highlighted lines:

      sfc-project/src/App.vue

      <template>
        <div class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation" class="card">
            <p>{{ airport.abbreviation }}</p>
            <p>{{ airport.name }}</p>
            <p>{{ airport.city }}, {{ airport.state }}</p>
          </div>
        </div>
      </template>
      
      <script>
      import { ref } from 'vue'
      import data from '@/data/us-airports.js'
      
      export default {
        name: 'App',
        setup() {
          const airports = ref(data)
      
          return { airports }
        }
      }
      </script>
      ...
      

      In this snippet, you imported the data and rendered it using <div> elements and the v-for directive in the template.

      At this point, the data is imported and ready to be used in the App.vue component. But first, add some styling to make the data easier for users to read. In this same file, add the following CSS in the <style> tag:

      sfc-project/src/App.vue

      ...
      <style>
      #app { ... }
      
      .wrapper {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 1rem;
        max-width: 960px;
        margin: 0 auto;
      }
      
      .card {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      
      .card p:first-child {
        font-weight: bold;
        font-size: 2.5rem;
        margin: 1rem 0;
      }
      
      .card p:last-child {
        font-style: italic;
        font-size: .8rem;
      }
      </style>
      

      In this case, you are using CSS Grid to compose these cards of airport codes into a grid of three. Notice how this grid is set up in the .wrapper class. The .card class is the card or section that contains each airport code, name, and location. If you would like to learn more about CSS, check out our How To Style HTML with CSS.

      Open your browser and navigate to localhost:8080. You will find a number of cards with airport codes and information:

      Three airport cards rendering the data for the US airports dataset

      Now that you have set up your initial app, you can refactor the data into a Single-File Component in the next step.

      Step 2 — Creating a Single-File Component

      Since Vue CLI uses Webpack to build your app into something the browser can read, your app can use SFCs or .vue files instead of plain JavaScript. These files are a way for you to create small blocks of scalable and reusable code. If you were to change one component, it would be updated everywhere.

      These .vue components usually consist of these three things: <template>, <script>, and <style> elements. SFC components can either have scoped or unscoped styles. When a component has scoped styles, that means the CSS between the <style> tags will only affect the HTML in the <template> in the same file. If a component has un-scoped styles, the CSS will affect the parent component as well as its children.

      With your project successfully set up, you are now going to break these airport cards into a component called AirportCards.vue. As it stands now, the HTML in the App.vue is not very reusable. You will break this off into its own component so you can import it anywhere else into this app while preserving the functionality and visuals.

      In your terminal, create this .vue file in the components directory:

      • touch src/components/AirportCards.vue

      Open the AiportCards.vue component in your text editor. To illustrate how you can re-use blocks of code using components, move most of the code from the App.vue file to the AirportCards.vue component:

      sfc-project/src/components/AirportCards.vue

      <template>
        <div class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation" class="card">
            <p>{{ airport.abbreviation }}</p>
            <p>{{ airport.name }}</p>
            <p>{{ airport.city }}, {{ airport.state }}</p>
          </div>
        </div>
      </template>
      
      <script>
      import { ref } from 'vue'
      import data from '@/data/us-airports.js'
      
      export default {
        name: 'Airports',
        setup() {
          const airports = ref(data)
      
          return { airports }
        }
      }
      </script>
      
      <style scoped>
      .wrapper {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 1rem;
        max-width: 960px;
        margin: 0 auto;
      }
      
      .card {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      
      .card p:first-child {
        font-weight: bold;
        font-size: 2.5rem;
        margin: 1rem 0;
      }
      
      .card p:last-child {
        font-style: italic;
        font-size: .8rem;
      }
      </style>
      

      Save and close the file.

      Next, open your App.vue file. Now you can clean up the App.vue component and import AirportCards.vue into it:

      sfc-project/src/App.vue

      <template>
        <AirportCards />
      </template>
      
      <script>
      import AirportCards from '@/components/Airports.vue'
      
      export default {
        name: 'App',
        components: {
          AirportCards
        }
      }
      </script>
      
      <style scoped>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      </style>
      

      Now that AirportCards is a standalone component, you have put it in the <template> HTML as you would a <p> tag.

      When you open up localhost:8080 in your browser, nothing will change. The same three airport cards will still display, because you are rendering the new SFC in the <AirportCards /> element.

      Next, add this same component in the template again to illustrate the re-usability of components:

      /src/App.vue

      <template>
        <AirportCards />
        <airport-cards />
      </template>
      ...
      

      You may notice that this new instance of AirportCards.vue is using kebab-case over PascalCase. When referencing components, Vue does not care which one you use. All capitalized words and letters will be separated by a hyphen and will be lower case. The same applies to props as well, which will be explained in the next section.

      Note: The case that you use is up to personal preference, but consistency is important. Vue.js recommends using kebab-case as it follows the HTML standard.

      Open the browser and visit localhost:8080. You will find the cards duplicated:

      The US airport cards rendered twice in the browser.

      This adds modularity to your app, but the data is still static. The row of cards is useful if you want to show the same three airports, but changing the data source would require changing the hard-coded data. In the next step, you are going to expand this component further by registering props and passing data from the parent down to the child component.

      Step 3 — Leveraging Props to Pass Down Data

      In the previous step, you created an AirportCards.vue component that rendered a number of cards from the data in the us-airports.js file. In addition to that, you also doubled the same component reference to illustrate how you can easily duplicate code by adding another instance of that component in the <template>.

      However, leaving the data static will make it difficult to change the data in the future. When working with SFCs, it can help if you think of components as functions. These functions are components that can take in arguments (props) and return something (HTML). For this case, you will pass data into the airports parameter to return dynamic HTML.

      Open the AirportCards.vue component in your text editor. You are currently importing data from the us-airports.js file. Remove this import statement, as well as the setup function in the <script> tag:

      sfc-project/src/components/AirportCards.vue

      ...
      <script>
      
      export default {
        name: 'Airports',
      }
      </script>
      ...
      

      Save the file. At this point, nothing will render in the browser.

      Next, move forward by defining a prop. This prop can be named anything; it just describes the data coming in and associates it with a name.

      To create a prop, add the props property in the component. The value of this is a series of key/value pairs. The key is the name of the prop, and the value is the description of the data. It’s best to provide as much description as you can:

      sfc-project/src/components/AirportCards.vue

      ...
      <script>
      
      export default {
        name: 'Airports',
        props: {
          airports: {
            type: Array,
            required: true
          }
        }
      }
      </script>
      ...
      

      Now, in AirportCard.vue, the property airports refers to the data that is passed in. Save and exit from the file.

      Next, open the App.vue component in your text editor. Like before, you will need to import data from the us-airports.js file and import the ref function from Vue to make it reactive for the HTML template:

      sfc-project/src/App.vue

      <template>
        <AirportCards :airports="usAirports" />
        <airport-cards />
      </template>
      
      <script>
      import { ref } from 'vue'
      import AirportCards from '@/components/Airports.vue'
      import usAirportData from '@/data/us-airports.js'
      
      export default {
        name: 'App',
        components: {
          AirportCards
        },
        setup() {
          const usAirports = ref(usAirportData)
      
          return { usAirports }
        }
      }
      </script>
      

      If you open your browser and visit localhost:8080, you will find the same US airports as before:

      US airports rendered on cards in the browser

      There is another AirportCards.vue instance in your template. Since you defined props within that component, you can pass any data with the same structure to render a number of cards from different airports. This is where the eu-airports.js file from the initial setup comes in.

      In App.vue, import the eu-airports.js file, wrap it in the ref function to make it reactive, and return it:

      /src/App.vue

      <template>
        <AirportCards :airports="usAirports" />
        <airport-cards :airports="euAirports" />
      </template>
      
      <script>
      ...
      import usAirportData from '@/data/us-airports.js'
      import euAirportData from '@/data/eu-airports.js'
      
      export default {
        ...
        setup() {
          const usAirports = ref(usAirportData)
          const euAirports = ref(euAirportData)
      
          return { usAirports, euAirports }
        }
      }
      </script>
      ...
      

      Open your browser and visit localhost:8080. You will find the European airport data rendered below the US airport data:

      US airport data rendered as cards, followed by European airport data rendered in the same way.

      You have now successfully passed in a different dataset into the same component. With props, you are essentially re-assigning data to a new name and using that new name to reference data in the child component.

      At this point, this application is starting to become more dynamic. But there is still something else that you can do to make this even more focused and re-usable. In Vue.js, you can use something called slots. In the next step, you are going to create a Card.vue component with a default slot that injects the HTML into a placeholder.

      Step 4 — Creating a General Card Component Using Slots

      Slots are a great way to create re-usable components, especially if you do not know if the HTML in that component will be similar. In the previous step, you created another AirportCards.vue instance with different data. In that example, the HTML is the same for each. It’s a <div> in a v-for loop with paragraph tags.

      Open your terminal and create a new file using the touch command. This file will be named Card.vue:

      • touch src/components/Card.vue

      In your text editor, open the new Card.vue component. You are going to take some of the CSS from AirportCards.vue and add it into this new component.

      Create a <style> tag and add in the following CSS:

      sfc-project/src/components/Card.vue

      <style>
      .card {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      </style>
      

      Next, create the HTML template for this component. Before the <style> tag, add a <template> tag with the following:

      sfc-project/src/components/Card.vue

      <template>
        <div class="card">
      
        </div>
      </template>
      ...
      

      In between the <div class="card">, add the <slot /> component. This is a component that is provided to you by Vue. There is no need to import this component; it is globally imported by Vue.js:

      sfc-project/src/components/Card.vue

      <template>
        <div class="card">
          <slot />
        </div>
      </template>
      

      This slot is a placeholder for the HTML that is between the Card.vue component’s tags when it is referenced elsewhere.

      Save and exit the file.

      Now go back to the AirportCards.vue component. First, import the new Card.vue SFC you just created:

      sfc-project/src/components/AirportCards.vue

      ...
      <script>
      import Card from '@/components/Card.vue'
      
      export default {
        name: 'Airports',
        props: { ... },
        components: {
          Card
        }
      }
      </script>
      ...
      

      Now all that is left is to replace the <div> with <card>:

      /src/components/AirportCards.vue

      <template>
        <div class="wrapper">
          <card v-for="airport in airports" :key="airport.abbreviation">
            <p>{{ airport.abbreviation }}</p>
            <p>{{ airport.name }}</p>
            <p>{{ airport.city }}, {{ airport.state }}</p>
          </card>
        </div>
      </template>
      ...
      

      Since you have a <slot /> in your Card.vue component, the HTML between the <card> tags is injected in its place while preserving all styles that have been associated with a card.

      Save the file. When you open your browser at localhost:8080, you will find the same cards that you’ve had previously. The difference now is that your AirportCards.vue now reference the Card.vue component:

      US airport data rendered as cards, followed by European airport data rendered in the same way.

      To show the power of slots, open the App.vue component in your application and import the Card.vue component:

      sfc-project/src/App.vue

      ...
      <script>
      ...
      import Card from '@/components/Card.vue'
      
      export default {
        ...
        components: {
          AirportCards,
          Card
        },
        setup() { ... }
      }
      </script>
      ...
      

      In the <template>, add the following under the <airport-cards /> instances:

      sfc-project/src/App.vue

      <template>
        <AirportCards :airports="usAirports"/>
        <airport-cards :airports="euAirports" />
        <card>
          <p>US Airports</p>
          <p>Total: {{ usAirports.length }}</p>
        </card>
        <card>
          <p>EU Airports</p>
          <p>Total: {{ euAirports.length }}</p>
        </card>
      </template>
      ...
      

      Save the file and visit localhost:8080 in the browser. Your browser will now render additional elements displaying the number of airports in the datasets:

      US and European airports displayed, along with two cards that display that there are three airports in each dataset

      The HTML between the <card /> tags is not exactly the same, but it still renders a generic card. When leveraging slots, you can use this functionality to create small, re-usable components that have a number of different uses.

      Conclusion

      In this tutorial, you created single-file components and used props and slots to create reusable blocks of code. In the project, you created an AirportCards.vue component that renders a number of airport cards. You then broke up the AirportCards.vue component further into a Card.vue component with a default slot.

      You ended up with a number of components that are dynamic and can be used in a number of different uses, all while keeping code maintainable and in keeping with the D.R.Y. software principle.

      To learn more about Vue components, it is recommended to ready through the Vue documentation. For more tutorials on Vue, check out the How To Develop Websites with Vue.js series page.



      Source link

      How To Create Custom Types 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.

      Though the pre-made, basic types in TypeScript will cover many use cases, creating your own custom types based on these basic types will allow you to ensure the type checker validates the data structures specific to your project. This will reduce the chance of bugs in your project, while also allowing for better documentation of the data structures used throughout the code.

      This tutorial will show you how to use custom types with TypeScript, how to compose those types together with unions and intersections, and how to use utility types to add flexibility to your custom types. 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.

      Creating Custom Types

      In cases where programs have complex data structures, using TypeScript’s basic types may not completely describe the data structures you are using. In these cases, declaring your own type will help you address the complexity. In this section, you are going create types that can be used to describe any object shape you need to use in your code.

      Custom Type Syntax

      In TypeScript, the syntax for creating custom types is to use the type keyword followed by the type name and then an assignment to a {} block with the type properties. Take the following:

      type Programmer = {
        name: string;
        knownFor: string[];
      };
      

      The syntax resembles an object literal, where the key is the name of the property and the value is the type this property should have. This defines a type Programmer that must be an object with the name key that holds a string value and a knownFor key that holds an array of strings.

      As shown in the earlier example, you can use ; as the separator between each property. It is also possible to use a comma, ,, or to completely omit the separator, as shown here:

      type Programmer = {
        name: string
        knownFor: string[]
      };
      

      Using your custom type is the same as using any of the basic types. Add a double colon and then add your type name:

      type Programmer = {
        name: string;
        knownFor: string[];
      };
      
      const ada: Programmer = {
        name: 'Ada Lovelace',
        knownFor: ['Mathematics', 'Computing', 'First Programmer']
      };
      

      The ada constant will now pass the type checker without throwing an error.

      If you write this example in any editor with full support of TypeScript, like in the TypeScript Playground, the editor will suggest the fields expected by that object and their types, as shown in the following animation:

      An animation showing suggestions to add the

      If you add comments to the fields using the TSDoc format, a popular style of TypeScript comment documentation, they are also suggested in code completion. Take the following code with explanations in comments:

      type Programmer = {
        /**
         * The full name of the Programmer
         */
        name: string;
        /**
         * This Programmer is known for what?
         */
        knownFor: string[];
      };
      
      const ada: Programmer = {
        name: 'Ada Lovelace',
        knownFor: ['Mathematics', 'Computing', 'First Programmer']
      };
      

      The commented descriptions will now appear with the field suggestions:

      Code completion with TSDoc comments

      When creating an object with the custom type Programmer, if you assign a value with an unexpected type to any of the properties, TypeScript will throw an error. Take the following code block, with a highlighted line that does not adhere to the type declaration:

      type Programmer = {
        name: string;
        knownFor: string[];
      };
      
      const ada: Programmer = {
        name: true,
        knownFor: ['Mathematics', 'Computing', 'First Programmer']
      };
      

      The TypeScript Compiler (tsc) will show the error 2322:

      Output

      Type 'boolean' is not assignable to type 'string'. (2322)

      If you omitted any of the properties required by your type, like in the following:

      type Programmer = {
        name: string;
        knownFor: string[];
      };
      
      const ada: Programmer = {
        name: 'Ada Lovelace'
      };
      

      The TypeScript Compiler will give the error 2741:

      Output

      Property 'knownFor' is missing in type '{ name: string; }' but required in type 'Programmer'. (2741)

      Adding a new property not specified in the original type will also result in an error:

      type Programmer = {
        name: string;
        knownFor: string[];
      };
      
      const ada: Programmer = {
        name: "Ada Lovelace",
        knownFor: ['Mathematics', 'Computing', 'First Programmer'],
        age: 36
      };
      

      In this case, the error shown is the 2322:

      Output

      Type '{ name: string; knownFor: string[]; age: number; }' is not assignable to type 'Programmer'. Object literal may only specify known properties, and 'age' does not exist in type 'Programmer'.(2322)

      Nested Custom Types

      You can also nest custom types together. Imagine you have a Company type that has a manager field that adheres to a Person type. You could create those types like this:

      type Person = {
        name: string;
      };
      
      type Company = {
        name: string;
        manager: Person;
      };
      

      Then you could create a value of type Company like this:

      const manager: Person = {
        name: 'John Doe',
      }
      
      const company: Company = {
        name: 'ACME',
        manager,
      }
      

      This code would pass the type checker, since the manager constant fits the type designated for the manager field. Note that this uses the object property shorthand to declare manager.

      You can omit the type in the manager constant because it has the same shape as the Person type. TypeScript is not going to raise an error when you use an object with the same shape as the one expected by the type of the manager property, even if it is not set explicitly to have the Person type

      The following will not throw an error:

      const manager = {
        name: 'John Doe'
      }
      
      const company: Company = {
        name: 'ACME',
        manager
      }
      

      You can even go one step further and set the manager directly inside this company object literal:

      const company: Company = {
        name: 'ACME',
        manager: {
          name: 'John Doe'
        }
      };
      

      All these scenarios are valid.

      If writing these examples in an editor that supports TypeScript, you will find that the editor will use the available type information to document itself. For the previous example, as soon as you open the {} object literal for manager, the editor will expect a name property of type string:

      TypeScript Code Self-Documenting

      Now that you have gone through some examples of creating your own custom type with a fixed number of properties, next you’ll try adding optional properties to your types.

      Optional Properties

      With the custom type declaration in the previous sections, you cannot omit any of the properties when creating a value with that type. There are, however, some cases that require optional properties that can pass the type checker with or without the value. In this section, you will declare these optional properties.

      To add optional properties to a type, add the ? modifier to the property. Using the Programmer type from the previous sections, turn the knownFor property into an optional property by adding the following highlighted character:

      type Programmer = {
        name: string;
        knownFor?: string[];
      };
      

      Here you are adding the ? modifier after the property name. This makes TypeScript consider this property as optional and not raise an error when you omit that property:

      type Programmer = {
        name: string;
        knownFor?: string[];
      };
      
      const ada: Programmer = {
        name: 'Ada Lovelace'
      };
      

      This will pass without an error.

      Now that you know how to add optional properties to a type, it is time to learn how to create a type that can hold an unlimited number of fields.

      Indexable Types

      The previous examples showed that you cannot add properties to a value of a given type if that type does not specify those properties when it was declared. In this section, you will create indexable types, which are types that allow for any number of fields if they follow the index signature of the type.

      Imagine you had a Data type to hold an unlimited number of properties of the any type. You could declare this type like this:

      type Data = {
        [key: string]: any;
      };
      

      Here you create a normal type with the type definition block in curly brackets ({}), and then add a special property in the format of [key: typeOfKeys]: typeOfValues, where typeOfKeys is the type the keys of that object should have, and typeOfValues is the type the values of those keys should have.

      You can then use it normally like any other type:

      type Data = {
        [key: string]: any;
      };
      
      const someData: Data = {
        someBooleanKey: true,
        someStringKey: 'text goes here'
        // ...
      }
      

      Using indexable types, you can assign an unlimited number of properties, as long as they match the index signature, which is the name used to describe the types of the keys and values of an indexable type. In this case, the keys have a string type, and the values have any type.

      It is also possible to add specific properties that are always required to your indexable type, just like you could with a normal type. In the following highlighted code, you are adding the status property to your Data type:

      type Data = {
        status: boolean;
        [key: string]: any;
      };
      
      const someData: Data = {
        status: true,
        someBooleanKey: true,
        someStringKey: 'text goes here'
        // ...
      }
      

      This would mean that a Data type object must have a status key with a boolean value to pass the type checker.

      Now that you can create an object with different numbers of elements, you can move on to learning about arrays in TypeScript, which can have a custom number of elements or more.

      Creating Arrays with Number of Elements or More

      Using both the array and tuple basic types available in TypeScript, you can create custom types for arrays that should have a minimum amount of elements. In this section, you will use the TypeScript rest operator ... to do this.

      Imagine you have a function responsible for merging multiple strings. This function is going to take a single array parameter. This array must have at least two elements, each of which should be strings. You can create a type like this with the folowing:

      type MergeStringsArray = [string, string, ...string[]];
      

      The MergeStringsArray type is taking advantage of the fact that you can use the rest operator with an array type and uses the result of that as the third element of a tuple. This means that the first two strings are required, but additional string elements after that are not required.

      If an array has less than two string elements, it will be invalid, like the following:

      const invalidArray: MergeStringsArray = ['some-string']
      

      The TypeScript Compiler is going to give error 2322 when checking this array:

      Output

      Type '[string]' is not assignable to type 'MergeStringsArray'. Source has 1 element(s) but target requires 2. (2322)

      Up to this point, you have created your own custom types from a combination of basic types. In the next section, you will make a new type by composing two or more custom types together.

      Composing Types

      This section will go through two ways that you can compose types together. These will use the union operator to pass any data that adheres to one type or the other and the intersection operator to pass data that satisfies all the conditions in both types.

      Unions

      Unions are created using the | (pipe) operator, which represents a value that can have any of the types in the union. Take the following example:

      type ProductCode = number | string
      

      In this code, ProductCode can be either a string or a number. The following code will pass the type checker:

      type ProductCode = number | string;
      
      const productCodeA: ProductCode="this-works";
      
      const productCodeB: ProductCode = 1024;
      

      A union type can be created from a union of any valid TypeScript types.

      Intersections

      You can use intersection types to create a completely new type that has all the properties of all the types being intersected together.

      For example, imagine you have some common fields that always appear in the response of your API calls, then specific fields for some endpoints:

      type StatusResponse = {
        status: number;
        isValid: boolean;
      };
      
      type User = {
        name: string;
      };
      
      type GetUserResponse = {
        user: User;
      };
      

      In this case, all responses will have status and isValid properties, but only user resonses will have the additional user field. To create the resulting response of a specific API User call using an intersection type, combine both StatusResponse and GetUserResponse types:

      type ApiGetUserResponse = StatusResponse & GetUserResponse;
      

      The type ApiGetUserResponse is going to have all the properties available in StatusResponse and those available in GetUserResponse. This means that data will only pass the type checker if it satisfies all the conditions of both types. The following example will work:

      let response: ApiGetUserResponse = {
          status: 200,
          isValid: true,
          user: {
              name: 'Sammy'
          }
      }
      

      Another example would be the type of the rows returned by a database client for a query that contains joins. You would be able to use an intersection type to specify the result of such a query:

      type UserRoleRow = {
        role: string;
      }
      
      type UserRow = {
        name: string;
      };
      
      type UserWithRoleRow = UserRow & UserRoleRow;
      

      Later, if you used a fetchRowsFromDatabase() function like the following:

      const joinedRows: UserWithRoleRow = fetchRowsFromDatabase()
      

      The resulting constant joinedRows would have to have a role property and a name property that both held string values in order to pass the type checker.

      Using Template Strings Types

      Starting with TypeScript 4.1, it is possible to create types using template string types. This will allow you to create types that check specific string formats and add more customization to your TypeScript project.

      To create template string types, you use a syntax that is almost the same as what you would use when creating template string literals. But instead of values, you will use other types inside the string template.

      Imagine you wanted to create a type that passes all strings that begin with get. You would be able to do that using template string types:

      type StringThatStartsWithGet = `get${string}`;
      
      const myString: StringThatStartsWithGet="getAbc";
      

      myString will pass the type checker here because the string starts with get then is followed by an additional string.

      If you passed an invalid value to your type, like the following invalidStringValue:

      type StringThatStartsWithGet = `get${string}`;
      
      const invalidStringValue: StringThatStartsWithGet="something";
      

      The TypeScript Compiler would give you the error 2322:

      Output

      Type '"something"' is not assignable to type '`get${string}`'. (2322)

      Making types with template strings helps you to customize your type to the specific needs of your project. In the next section, you will try out type assertions, which add a type to otherwise untyped data.

      Using Type Assertions

      The any type can be used as the type of any value, which often does not provide the strong typing needed to get the full benefit out of TypeScript. But sometimes you may end up with some variables bound to any that are outside of your control. This will happen if you are using external dependencies that were not written in TypeScript or that do not have type declaration available.

      In case you want to make your code type-safe in those scenarios, you can use type assertions, which is a way to change the type of a variable to another type. Type assertions are made possible by adding as NewType after your variable. This will change the type of the variable to that specified after the as keyword.

      Take the following example:

      const valueA: any = 'something';
      
      const valueB = valueA as string;
      

      valueA has the type any, but, using the as keyword, this code coerces the valueB to have the type string.

      Note: To assert a variable of TypeA to have the type TypeB, TypeB must be a subtype of TypeA. Almost all TypeScript types, besides never, are a subtype of any, including unknown.

      Utility Types

      In the previous sections, you reviewed multiple ways to create custom types out of basic types. But sometimes you do not want to create a completely new type from scratch. There are times when it might be best to use a few properties of an existing type, or even create a new type that has the same shape as another type, but with all the properties set to be optional.

      All of this is possible using existing utility types available with TypeScript. This section will cover a few of those utility types; for a full list of all available ones, take a look at the Utility Types part of the TypeScript handbook.

      All utility types are Generic Types, which you can think of as a type that accepts other types as parameters. A Generic type can be identified by being able to pass type parameters to it using the <TypeA, TypeB, ...> syntax.

      Record<Key, Value>

      The Record utility type can be used to create an indexable type in a cleaner way than using the index signature covered previously.

      In your indexable types example, you had the following type:

      type Data = {
        [key: string]: any;
      };
      

      You can use the Record utility type instead of an indexable type like this:

      type Data = Record<string, any>;
      

      The first type parameter of the Record generic is the type of each key. In the following example, all the keys must be strings:

      type Data = Record<string, any>
      

      The second type parameter is the type of each value of those keys. The following would allow the values to be any:

      type Data = Record<string, any>
      

      Omit<Type, Fields>

      The Omit utility type is useful to create a new type based on another one, while excluding some properties you do not want in the resulting type.

      Imagine you have the following type to represent the type of a user row in a database:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      

      If in your code you are retrieving all the fields but the addressId one, you can use Omit to create a new type without that field:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      
      type UserRowWithoutAddressId = Omit<UserRow, 'addressId'>;
      

      The first argument to Omit is the type that you are basing the new type on. The second is the field that you’d like to omit.

      If you hover over UserRowWithoutAddressId in your code editor, you will find that it has all the properties of the UserRow type but the ones you omitted.

      You can pass multiple fields to the second type parameter using a union of strings. Say you also wanted to omit the id field, you could do this:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      
      type UserRowWithoutIds = Omit<UserRow, 'id' | 'addressId'>;
      

      Pick<Type, Fields>

      The Pick utility type is the exact opposite of the Omit type. Instead of saying the fields you want to omit, you specify the fields you want to use from another type.

      Using the same UserRow you used before:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      

      Imagine you need to select only the email key from the database row. You could create such a type using Pick like this:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      
      type UserRowWithEmailOnly = Pick<UserRow, 'email'>;
      

      The first argument to Pick here specifies the type you are basing the new type on. The second is the key that you would like to include.

      This would be equivalent to the following:

      type UserRowWithEmailOnly = {
          email: string;
      }
      

      You are also able to pick multiple fields using an union of strings:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      
      type UserRowWithEmailOnly = Pick<UserRow, 'name' | 'email'>;
      

      Partial<Type>

      Using the same UserRow example, imagine you want to create a new type that matches the object your database client can use to insert new data into your user table, but with one small detail: Your database has default values for all fields, so you are not required to pass any of them. To do this, you can use a Partial utility type to optionally include all fields of the base type.

      Your existing type, UserRow, has all the properties as required:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      

      To create a new type where all properties are optional, you can use the Partial<Type> utility type like the following:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      
      type UserRowInsert = Partial<UserRow>;
      

      This is exactly the same as having your UserRowInsert like this:

      type UserRow = {
        id: number;
        name: string;
        email: string;
        addressId: string;
      };
      
      type UserRowInsert = {
        id?: number | undefined;
        name?: string | undefined;
        email?: string | undefined;
        addressId?: string | undefined;
      };
      

      Utility types are a great resource to have, because they provide a faster way to build up types than creating them from the basic types in TypeScript.

      Conclusion

      Creating your own custom types to represent the data structures used in your own code can provide a flexible and useful TypeScript solution for your project. In addition to increasing the type-safety of your own code as a whole, having your own business objects typed as data structures in the code will increase the overall documentation of the code-base and improve your own developer experience when working with teammates on the same code-base.

      For more tutorials on TypeScript, check out our TypeScript Topic page.



      Source link