Back

TypeScript Basics

Published: 11/01/24

Back to top

What is TypeScript

For example, here's some TypeScript:

let firstName: string = "Sammy";

The : string is TypeScript syntax which ensures that this variable will only ever contain a value of type string

If another developer attempts to assign a value to this variable of type number, TypeScript will throw an error in VS Code and won't compile via the tsc CLI

Setup a Local Environment

This section guides you through how to install and compile TypeScript code.

Create a folder to practice TypeScript

On your desktop create a typescript directory:

Setup a package.json file

npm init -y

Install TypeScript as a dev dependency

We use TypeScript during development (not to actually run the app), so we'll add the --save-dev flag to indicate that it's a dependency for development

npm install typescript --save-dev

We should now be able to run the cli

npx tsc

Which should output something like Version 5.3.3

Create a tsconfig.json file

We can control how the TypeScript compiler behaves via a JSON file, let's create it

npx tsc --init

This will generate a tsconfig.json file with some default settings

Let's add a setting to tell the TypeScript compiler where to find our TypeScript files and where to send our compiled JavaScript files

{
  "include": ["src"],
  "compilerOptions": {
    "outDir": "./build",
    ...
  }
}

Now create an src/ directory and inside of it create a index.ts

Add

let firstName: string = "Sammy";

inside the index.ts and run

npx tsc

You should now see a build directory with an index.js file inside.

Let's run

npx tsc --watch

Which will watch for changes in the TypesScript file and automatically transpile it to index.js

Vanilla TypeScript

Here are a few of the core features of TypeScript.

For a more detailed breakdown, take a look at the TypeScript Documentation.

Typing Variables

When creating variables, we can either provide an explicit type assignment:

let firstName: string = "Sammy";

In the code above, we're creating a variable and explicity telling TypeScript (via : string) that it must be a string.

TS Playground - Variables

This makes your code easier to read/understand, and is more explicit.

You can also create a variable without a type and TypeScript will infer (i.e. automatically detect) the type, based on the value:

let surname = "Abukmeil";

This is called an implicit type assignment.

You can hover over a variable in VS Code to check what type TypeScript inferred:

VS Code type popup

If you try to assign a value with the wrong type to a typed variable, TypeScript will highlight an error in VS Code (squiggly red line) and the TypeScript compiler will throw an error:

Type 'number' is not assignable to type 'string'

TS Playground - Incorrect Type Error

In TS Playground, you'll see the errors in two places:

  1. In the panel on the left by hovering over the red squiggly line
  2. In the panel on the right by switching to the "Errors" tab

This happens with both explicity typed variables and implicity typed variables.

Typing Arrays

When creating arrays, we can add a type like so

const games: string[] = ["Space Invaders", "Pac Man"];

The : string[] above says "this array can only contain strings"

TS Playground - Arrays

If we try to do the following

games.push(4);

We get the following error

Argument of type 'number' is not assignable to parameter of type 'string'

Tuples

A tuple is an array with a specific length & specific types for each index

let inventoryItem: [number, string, number] = [1, "Gaming Monitor", 100];

The : [number, string, number] above says "This array must have 3 items with the following types"

TS Playground - Tuples

So if we were to re-assign this variable, this is allowed:

inventoryItem = [2, "Microwave", 40];

Whereas this isn't (since the last element in the array should be a number):

inventoryItem = [2, "Toaster", "35"];

We get the following error Type 'string' is not assignable to type 'number'

When creating tuples, you can also give a name to each array item for clarity

let inventoryItem: [id: number, name: string, price: number] = [
  1,
  "Gaming Monitor",
  100,
];

Typing Objects

When creating objects, we can add a type like so

const game: { name: string; releaseYear: number } = {
  name: "Resident Evil",
  releaseYear: 1996,
};

where : { name: string; releaseYear: number } specifies the name and type for each property

TS Playground Example - Typing an object

Enums

An enum is a group of constants (i.e. variables that should never change).

For example

enum Directions {
  North,
  East,
  South,
  West,
}

In the code above, we've defined some directions that we'll use in our app.

let currentDirection = Directions.North;

TS Playground Example - Enum

In the code above, we're creating a variable and setting it equal to one of our enum values.

If we console.log() this variable, we'll get 0

console.log(currentDirection); // 0

So each value in our enum is given a number starting from 0.

Now that variable can only ever be assigned a value from this enum.

If we try to set the variable to some other value

currentDirection = "Something Incorrect";

We'll get the following error Type '"Something Incorrect"' is not assignable to type 'Directions'

We can also change the numbers that the enum values are linked to

enum StatusCodes {
  Success = 200,
  NotFound = 404,
  ServerError = 500,
}

const currentStatus = StatusCodes.NotFound;

console.log(currentStatus); // 404

Or change the numbers that the enum values are linked to to be strings instead

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

const currentDirection = Direction.Up;

console.log(currentDirection); // UP

Type Aliases

We can define a Type Alias which makes types reusable and gives them a name.

type GameReleaseYear = number;

const pacmanYear: GameReleaseYear = 1980;
const spaceInvadersYear: GameReleaseYear = 1978;

TS Playground Example - Type Alias

In the code above, we created a Type Alias called GameReleaseYear which is a number.

This Type Alias is used for multiple variables.

We can define a Type Alias for any type, including objects:

type Game = {
  title: string;
  year: number;
};

const game: Game = {
  title: "Pac Man",
  year: 1980,
};

TS Playground Example - Type Alias for an object

Interfaces

An interface is similar to a type alias, however, you can only use them for objects

interface Game {
  title: string;
  year: number;
}

const game: Game = {
  title: "Pac Man",
  year: 1980,
};

TS Playground Example - Interface

Interfaces have an additional feature compared to Type Aliases, that they can extend other interfaces:

interface Game {
  title: string;
  year: number;
}

interface IndieGame extends Game {
  developer: string;
}

const indieGame: IndieGame = {
  title: "Stardew Valley",
  year: 2016,
  developer: "ConcernedApe",
};

Union Types

A union type allows a value to be more than one type

function displayId(id: number | string) {
  console.log(`ID is: ${id}`);
}

displayId(5);
displayId("ff72ce73-322d-4eb2-8029-c739d30c3ef5");

TS Playground Example - Union Type

In the code above, the id property could either be a string or a number

Typing Functions

We can specify what the return value type will be:

function getUserId(): number {
  return 5;
}

The : number above says "this function will return a value of type number"

We can add types for each of the parameters:

function sum(x: number, y: number): number {
  return x + y;
}

TS Playground Example - Typing Functions

In the code above, we've added types for the parameters and the return type via : number

We can use a type alias to define the function types separately:

type Multiply = (x: number, y: number) => number;

const multiply: Multiply = (x, y) => {
  return x * y;
};

Typing Classes

When creating classes, we can add types for the properties and the methods

class Car {
  engineType: string;

  constructor(engineType: string) {
    this.engineType = engineType;
  }

  getEngineType(): string {
    return this.engineType;
  }
}

const car = new Car("Six cylinders");

In the code above, we specify that the class has a property called engineType which is a string, and that the getEngineType() method returns a string

We can also use private and public keywords to specify if certain properties / methods are accessible outside of the class

class Car {
  private engineType: string;

  constructor(engineType: string) {
    this.engineType = engineType;
  }

  public getEngineType(): string {
    return this.engineType;
  }
}

const car = new Car("Six cylinders");
console.log(car.getEngineType());
console.log(car.engineType); // Error

In the code above, the engineType property is set private and therefore only accessible within the class, whereas the getEngineType() method is public and can be accessed outside of the class.

Instead of typing private engineType: string; outside of the constructor and engineType: string which is repetitive, we can combine both into the constructor:

class Car {
  public constructor(private engineType: string) {
    this.engineType = engineType;
  }

  public getEngineType(): string {
    return this.engineType;
  }
}

const car = new Car("Six cylinders");

TS Playground Example - Typing Classes

In the above code private engineType: string defines a property and constructs it.

Generics

Generic Functions

Generics allow your typed functions to accept any type

function getValue<Type>(value: Type): Type {
  return value;
}

In the code above, we define a generic function which can receive a parameter of any type, and return a value of any type.

const num = getValue<number>(10);

In the code above, we're using the generic function, giving a type of number

const str = getValue<string>("Hello");

TS Playground Example - Generic Function

In the code above, we're using the same generic function, giving a type of string

Here's another example

function reverse<Type>(array: Type[]): Type[] {
  return array.reverse();
}

const reversedNumbers = reverse<number>([1, 2, 3]);
const reversedStrings = reverse<string>(["Hello", "Bye"]);

TS Playground Example - Generic Function Example 2

In the code above, the parameter array is typed as Type[] i.e. an array of whichever type was declared. The return value is also typed as Type[] i.e. it will return an array of the declared type

Generic Type Aliases

When creating a type alias (See section "Type Aliases" above for a refresher), we can make it generic

type Person<Type> = {
  name: string;
  age: number;
  job: Type;
};

In the code above, we've defined a generic type, where the job property could be any type

const personOne: Person<boolean> = {
  name: "Jane",
  age: 21,
  job: false,
};

In the code above, we're using our generic type alias and specifying that the job property will be a boolean

const personTwo: Person<string> = {
  name: "John",
  age: 35,
  job: "Doctor",
};

TS Playground Example - Generic Type Alias

In the code above, we've used the same generic type alias, this time job will be a string

TypeScript With React

Here are a few way of using TypeScript in React

For a more detailed breakdown, take a look at the React Documentation.

Creating a Vite app

From this tutorial, we'll use Vite.

We can create a new Vite app with TypeScript by running the following command

npm create vite@latest

Choose the following options:

Note: Another common approach is to use Next JS

File Extension

Ensure your file extensions are .tsx (it wont work with .js or .jsx)

Typing Props

Let's start with an example:

// App.tsx
function App() {
  const handleTodo = (text: string) => {
    console.log(text);
  };

  return (
    <>
      <NewTodo handleTodo={handleTodo} />
    </>
  );
}
// NewTodo.tsx
interface Props {
  handleTodo: (text: string) => void;
}

function NewTodo({ handleTodo }: Props) {
  return (
    <>
      <button onClick={() => handleTodo("Hello")}>Click</button>
    </>
  );
}

Here we've used an interface to define a type for the props object.

In this case, there's only one key value pair:

Typing State

Let's start with an example:

interface Todo {
  id: string;
  text: string;
}

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);

  return (
    <>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <button
        onClick={() => setTodos([...todos, { id: uuid(), text: "Hello" }])}
      >
        Add todo
      </button>
    </>
  );
}

Here we've used an interface to define a type for the state variable.

Todo[] means this state variable will contain an array of Todo type objects

useState() can be used as a Generic Function (as explained here) hence the following syntax useState<Todo[]>([])

TypeScript With Express

Here are a few way of using TypeScript in Express

Setting up an Express TypeScript app

Create an express app as usual:

npm init -y
npm i express

Install TypeScript as a dev dependancy, as well as some declaration packages for node and express

npm i -D typescript @types/express @types/node

These declaration packages (e.g. @types/express) can be found in the DefinitelyTyped GitHub Repo, which is a large collection of TypeScript type definitions.

For example, here are the type definitions for Express JS: DefinitelyTyped/types/express

Running npm i -D @types/express brings these into your node_modules/ directory and makes them usable in your application.

Create a tsconfig.json file

npx tsc --init

and ensure you set a directory for your compiled JS files to be sent to

{
  "compilerOptions": {
    ...
    "outDir": "./dist"
    ...
  }
}

This is how we'll run our app:

Since node can't run .ts files natively, we need to install a library for executing TypeScript via node called ts-node

npm i -D ts-node

And to watch for file changes in development, we'll use nodemon

npm i -D nodemon

And setup some scripts to run our app

{
  "scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts"
  }
}

Typing Core Express Objects

Create a src directory and a index.ts file inside it with the following:

import express, { Express, Request, Response } from "express";

const app: Express = express();

app.get("/", (req: Request, res: Response) => {
  res.send("Express + TypeScript Server");
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

Here we're importing types for:

These are imported from the express package, which will search from them in @types/express