TypeScript Tutorial: How To Use TypeScript In Practice?

Krystian Siwek
29 September 2020 · 10 min read

After reading the previous article aboutwhy you should start using TypeScript, you probably asked yourself - “ok, but how do I use TypeScript in practice?”. This TypeScript tutorial will help you learn how to use this programming language step by step.

TypeScript tutorial: Basic types

You probably already know most of the basic types if you are familiar with JavaScript, simply because TypeScript uses the same scalar types. But it also adds a few new ones. Let’s look at some examples featuring already-known types:

const name: string = 'Morty';
const age: number = 14;
const isAlive: boolean = true;

Note: number type also accepts hexadecimal, binary, and octal literals. If you want to define BigInteger use bigint type.

But what if we want to create a variable that’s an array of certain type values? There are two ways to do that.

`const numbers: number[] = [1, 3, 4, 2, 0];`\
`// or`\
`const numbers: Array<number> = [1, 3, 4, 2, 0];`\
`// or - when you want array of different type values`\
`const manyThings: (number|string|boolean)[];`

You also sometimes come across an undefined or null value. They also have their own types in TypeScript, named exactly the same.

const nullValue: null = null;
const undefinedValue: undefined = undefined;

Now we’ve introduced you to widely known types, we can start to focus on the new ones added by TypeScript. A few of them help us with typing.

  • Tuple

Tuple type allows you to define an array with a fixed length of elements, which types are selected and can’t be different. When initializing a tuple value, the values must be in the order provided when defining tuple.

`const nameAge: [string, number] = ['Rick', 70];

  • Enum

Next, we have a really useful addition to standard datatypes provided by JavaScript, called enum. Many object-oriented programming languages use enums. They let us declare a set of named constants like a collection of related values, used when a variable can only take its value from a small set like the defined enum. By default, enums start numbering from 0, but we can also manually set the value or values after it will increment by one.

enum Answer {
    Yes,
    No,
    Maybe
}
let questionAnswer: Answer = Answer.No; // questionAnswer is 1

// changing the values of enum
enum Answer {
    Yes = 1,
    No, // No equals 2
    Maybe = 6,
    DontKnow // DontKnow equals 7
}

// custom values
enum Answer {
    Yes = 'yes',
    No = 'no',
    Maybe = 'maybe'
}

When using custom values for enums, if we accidentally use pure ‘yes’ instead of Answer.Yes, it will also go through as some kind of if statement.

  • Any

Any type probably shouldn’t be used often. It accepts any type variable and we can assign variables typed with any to all other types. One of the few examples of using any type and avoiding type-checking would be using a 3rd party library that doesn’t support TypeScript. Or maybe if one of the components has an element that returns event on click and we don’t want to create a separate type/interface for that. Sometimes we just know better than TS. Any type serves as a way to skip using type-checking similar to //@ts-ignore.

`let acceptAny: any = 'string';`\
`acceptAny = 1;`\
`acceptAny = false;`\
`// everything goes through without error`\
``\
`let booleanValue: boolean = acceptAny;`\
`// goes through without error

  • Unknown

Sometimes we have a variable that we don’t know the type of - maybe we let the user dynamically choose it, or we want to accept all the types on a field from an external API. In situations like this we can use the unknown type to let everybody know that the value could be anything. This is the opposite of any - we can’t assign an unknown type variable to another variable.

let acceptAny: unknown = 'string';
acceptAny = 1;
acceptAny = false;
// everything goes through without error

let booleanValue: boolean = acceptAny;
// throws error - Type 'unknown' is not assignable to type 'boolean'.

  • Void

Void is often used in functions or props to show that they don’t return anything. When used in variables, you can only assign null or undefined to them.

function logInfo(): void {
  console.log('Information message');
}

interface ComponentProps {
   onSubmit: (success: boolean) => void;
}

  • Never

This is used for values that never occur. It’s similar to void, but generally used for functions that throw an exception or have an infinite loop.

function throwError(msg: string): never {
  throw new Error(msg);
}

function infinite(): never {
    while (true) {}
}

Note: never type is a subtype of every type and is assignable to every type, but other types are not subtypes of never and are not assignable to it.

Typescript tutorial: Interface

One of my favorite features of TypeScript is interfaces. We use them to define the structure the object we assign it to should follow. They come handy when typing responses from an API. Let’s look at an example from the Rick and Morty API.

{
    id: 2,
    name: "Morty Smith",
    status: "Alive",
    species: "Human",
    type: "",
    gender: "Male",
    origin: {
        name: "Earth (C-137)",
        url: "https://rickandmortyapi.com/api/location/1"
    },
    location: {
        name: "Earth (Replacement Dimension)",
        url: "https://rickandmortyapi.com/api/location/20"
    },
    image: "https://rickandmortyapi.com/api/character/avatar/2.jpeg",
    episode: [
        "https://rickandmortyapi.com/api/episode/1",
        "https://rickandmortyapi.com/api/episode/2",
        "https://rickandmortyapi.com/api/episode/3",
        ...
    ],
    url: "https://rickandmortyapi.com/api/character/2",
    created: "2017-11-04T18:50:21.651Z"
}

Here’s the response we get when we make a GET request for a character with an id of 2. We can make an interface for it and keep the code consistent.

type urlString = string

interface Origin {
    name: string,
    url: urlString
}

interface Planet {
    name: string,
    url: urlString
}

interface Character {
    id: number,
    name: string,
    status: string,
    species: string,
    type: string,
    gender: string,
    origin: Origin,
    location: Planet,
    image: urlString,
    episode: urlString[],
    url: urlString,
    created: string
}

It is as simple as that to create an interface for our response from an API. As you can see with Origin and Planet we can nest interfaces inside each other. We have also defined our type - urlString so we don’t confuse it with other strings. What if we want to create a new character ourselves but we want to skip some of the pieces of information? With a question mark, we can make a property optional.

interface Planet {
    name: string,
    url?: urlString
}

TypeScript also comes with an option to make some of the properties non-modifiable. We just have to put a readonly keyword before them. There is also ReadonlyArray type to use on an array variable alone.

type episodeList = ReadonlyArray<urlString>

interface Character {
    readonly id: number,
    episode: episodesList,
    ...
}

By making the episode property an episodeList type, we cannot modify things inside it but we can reassign an array (and only an array) value to it.

You may also want to provide some excess properties, for example for some of the locations. You can do that and also check for types of them, or use any if they can be a variety of things. You can also do a type assertion to check the types of properties that occur in an object.

// type assertion
interface Planet {
    name: string,
    url?: urlString
}
let earth = {name: 'Earth', population: 7500000000} as Planet
// goes through without error because name is present and url property is optional

let mars: Planet = {name: 'Mars', population: 0}
// error - Type '{ name: string; population: number; }' is not assignable to type 'Planet'. 
// Object literal may only specify known properties, and 'population' does not exist in type 'Planet'.

interface Planet {
    name: string,
    url?: urlString,
    [propName: string]: any
}
let earth: Planet = {name: 'Earth', population: 7500000000}

// using this notation we can also fastly define type for object(not the best practice though)
function parseObject(obj: { [key: string]: any }): number {
    // this functions accepts object and returns a number
}

To avoid code duplication we can use the extends keyword. This way our interface takes props from the parent and we can add other ones, something that’s very useful for Planet and Origin interfaces.

interface Origin {
    name: string,
    url?: urlString
}

interface Planet extends Origin {
    [propName: string]: any
}
This way we end up with interfaces that look like this.
type urlString = string
type episodeList = ReadonlyArray<urlString>

interface Origin {
    name: string,
    url?: urlString
}

interface Planet extends Origin {
    [propName: string]: any
}

interface Character {
    readonly id: number,
    name: string,
    status: string,
    species: string,
    type: string,
    gender: string,
    origin: Origin,
    location: Planet,
    image: urlString,
    episode: episodeList,
    url: urlString,
    created: string
}

Using Vue and React TypeScript

When we are using TypeScript with Vue or React we are almost always using some external libraries. The DefinitelyTyped repository on Github stores type definitions for thousands of libs and automatically publishes them to NPM. If we want to install types for a selected library, the name is always @types/{lib_name}. Let’s take Vue as an example:

  • npm Typescript

  npm install --save-dev @types/vue
  • yarn

  yarn add @types/vue --dev

Now, let’s compare the usage of TypeScript in two really simple button components that can do the same thing. One is written in Vue 3, the other one in React.

React TypeScript in action

import React, { useState } from "react";

interface IBtnProps {
  text: string;
  action: (clicked: boolean) => void;
}

interface IBtnState {
  state: boolean;
}

const ButtonComponent = ({ text, action }: IBtnProps) => {
  let [clicked, setClicked] = useState<IBtnState>({ state: false });

  return (
    <button
      onClick={() => {
        setClicked({ state: !clicked.state });
        action(clicked.state);
      }}
    >
      {text}
    </button>
  );
};

export default ButtonComponent;

As you can see, we created an interface for props that are being passed to our component. Normal text prop has to be the type of string. Action prop is a function that takes a boolean value as an argument and doesn’t return anything, so the void type is used. There is also an interface for our useState hook, which makes sure we always pass boolean to it.

We can also use TypeScript in class-based components.

import React from "react";

interface IBtnProps {
  text: string;
  action: (clicked: boolean) => void;
}

interface IBtnState {
  clicked: boolean;
}

export class ButtonComponent extends React.Component<IBtnProps, IBtnState> {
  state = { clicked: false };

  setClicked(btnState: boolean): void {
    this.setState({
      clicked: btnState,
    });
  }

  render(): React.ReactNode {
    return (
      <button
        onClick={() => {
          this.setClicked(!this.state.clicked);
          this.props.action(this.state.clicked);
        }}
      >
        {this.props.text}
      </button>
    );
  }
}

The interfaces for props and state are passed in arrow brackets when the component class is being created. When we specify the state in it, we don’t have to use type again on that variable. For the render function, the result is ReactNode from the standard react library.

Vue TypeScript in action (Vue 3)

<template>
  <button
    @click="
      clicked = !clicked;
      action(clicked);
    "
  >
    {{ text }}
  </button>
</template>
<script lang="ts">
import {
  defineComponent, PropType, ref, Ref,
} from 'vue';

interface Action {
  (clicked: boolean): void;
}

export default defineComponent({
  props: {
    text: {
      type: String,
    },
    action: {
      type: Function as PropType<Action>,
    },
  },
  setup(): { clicked: Ref<boolean> } {
    const clicked = ref<boolean>(false);

    return {
      clicked,
    };
  },
});
</script>

To work with TypeScript in Vue, we need to specify lang="ts"in the script tag. Action interface is something new - it uses the interface to describe a function that needs to be passed as an action prop. We also specify boolean type in the ref variable, using angle brackets to avoid changing it to another type.

TypeScript tutotial: Import

Let’s assume we have a few .tsfiles with different interfaces in a types directory. We can name them interfaces.ts and enums.ts, but we want to import interfaces, enums, types, etc. from one main filetypes.ts. In a situation like this, we have to use re-exports.

interfaces.ts

interface Action {
  (clicked: boolean): void;
}

export { Action };

enums.ts

enum State {
    Clicked,
    NotClicked,
}

export { State };

types.ts

`export * from './types/interfaces';
`export * from './types/enums';

This way we can easily import them in components from one file, keeping the code clean.

This is probably most of the basic things that you need to know when starting to use TypeScript. Of course, TS provides a lot more advanced features, things like Classes, Unions, Intersections, and something called Generics - we’ll dive into that one in the future.

TypeScript provides a wide range of useful and productivity-boosting tools. So stay ahead of technology trends and start using it today!

Want to learn more about JavaScript?

Go to JavaScript section

Related blogposts:

A11y: Web accessibility guidelines

Text similarity search using Elasticsearch and Python

GraphQL from Django developer perspective

Share on
Related posts
How To Automate Manual Tests With Cypress?
JAVASCRIPT

How To Automate Manual Tests With Cypress?

Manual testing is the foundation of testing - it’s always a start point. Why? Because it is simple, easy and fast. And that's what we expect from it. At least at the beginning. It gives us the answer…
4 min read

Talk to us about your project

Get in touch with us and find out how we can help you develop your software
Contact us