Telerik blogs

TypeScript is basically JavaScript with a type system, and in this post we’ll cover some of the most fundamental concepts of TypeScript.

JavaScript is undoubtedly one of the most popular programming languages of the decade, particularly among web and mobile application developers. Unfortunately, JavaScript was designed for prototypes and middle-sized applications, not large-scale and robust software applications. This was evident in its design system’s flaws and inadequacies, such as dynamic (loose) typing, implicit type coercion, etc.

TypeScript is a statically typed, open-source programming language introduced by Microsoft in October 2012. Fundamentally, it provides optional typing to JavaScript. The TypeScript programming language builds on top of JavaScript by adding optional static types, making it a typed superset of JavaScript.

TypeScript is JavaScript with a type system, and in this post we’ll cover some of the most fundamental concepts of TypeScript.

The ‘Why’ of TypeScript

  1. Static types: TypeScript improves the development experience (DX) by providing tools that enhance code uniformity and quality. TypeScript also provides documentation as it describes the inputs and outputs of a function and the structure of data in our application.
  2. Predictable code, easier to debug and scale: Though JavaScript is great for the flexibility it gives to its users, it can reach a point when it becomes messy, unreliable and buggy. To this end, TypeScript can help provide the necessary safety to organize code and catch bugs early enough before runtime.
  3. Using the latest features: JavaScript has different versions, some of which are yet to be fully supported by some browsers. TypeScript allows us to work with Javascript’s most recent, modern features without worrying about browser support. It can automatically close the gap between the different JavaScript versions during its compilation process.
  4. Others include IntelliSense (intelligent code completion), early bug detection, confidence when refactoring, etc.

Prerequisites

Before we can start developing apps with TypeScript, we need to have the following installed:

Node (npm)

Node is a JavaScript runtime environment. It ships with a tool called npm, which is short for Node Package Manager, responsible for maintaining a list of the packages (external libraries). Run this command to check if you already have npm installed:

npm -v

TypeScript Compiler

There are different ways of integrating TypeScript into your program. To keep things simple in this tutorial, we will install it using npm. Run this command in your terminal to check if you already have it installed:

tsc -v

If installed, you should see the version number printed; else, an error message is displayed.

If you don’t have it installed, run this command to install the TypeScript compiler globally on your computer:

npm install -g typescript

Upon successful installation, run this command to confirm the installation:

tsc -v

Code Editor/IDE

We will use the Visual Studio Code editor for this tutorial since it is very popular among developers, has a very useful integrated terminal and has built-in support for TypeScript. You can also use any other IDE of your choice.

Project Setup

First, we need to create a workspace containing a file where we can write our TypeScript program. To do that, run the following command in your terminal:

mkdir typescript-tutorial
cd typescript-tutorial
touch main.ts

Files ending with .ts extension tell VS Code that it is a TypeScript file. Launch the VS Code app, then copy and paste the code below to the main.ts file:

function greetings() {
  console.log("Hello, TypeScript!");
}
greetings();

Compile a Basic TypeScript Program

To open the VS Code integrated terminal, press CTRL (or CMD) + ` on your keyboard. The integrated terminal should open at the bottom of the VS Code app, automatically pointing to your current working directory.

Now run this command to compile our file:

tsc main.ts

This command uses the TypeScript compiler we installed globally to compile our TypeScript code to a regular JavaScript code. You will notice that the compiler created a new file in the same directory as your TypeScript file named main.js.

To execute this file, use the node command followed by the file name:

node main.js

You should see the message "Hello, TypeScript” printed on your console.

Type Annotation

TypeScript allows us to explicitly describe the type of an identifier (variables, function parameters and object properties). Type annotation in TypeScript is done using the “: type” syntax after an identifier, where “type” can be any valid type (string, number, boolean, etc.). Once an identifier is annotated as a string, only string values can be assigned to that identifier. The TypeScript compiler will throw an error if we attempt to assign a value other than a string to that identifier.

// After a variable name, we write a colon ":" and they a type.
let variableName: type = value;
const constantName: type = value;

The type of the identifier comes after your identifier (variable) name and is preceded by a colon.

Basic Types (Primitives)

Let’s take a look at how to add types to primitives.

let id: number = 26; // id is set to number
let name: string = "John Doe"; // name is of type string
let isLoggedIn: boolean = true; // isLoggedIn is set to boolean

In the above example, the variables id, name and isLoggedIn are annotated as number, string and boolean. As a result, any attempt to assign a value not of the type mentioned above would result in a type error. For example:

id = "twenty six"; // Error: Type 'string' is not assignable to type 'number'.

Essentially, TypeScript is giving us feedback directly in our code editor to let us know that we are attempting to assign a value of type string to a variable annotated as number, which isn’t allowed.

Reference Type

Objects

Objects in JavaScript can have as many properties as possible. In TypeScript, we type an object by type-annotating its individual properties.

let person: {
  name: string,
  age: number,
  isLoggedIn: boolean,
};

person = {
  name: "John Doe",
  age: 24,
  isLoggedIn: true,
};

As seen above, lines 1 to 5 define the type of the person object. The type annotation ensures that we don’t accidentally supply fewer or more properties during the assignment and that each property receives the correct type of value. Consider the following:

person = {
  name: "John Doe",
  age: 25,
};

// Error: Property 'isLoggedIn' is missing in type '{ name: string; age: number; }' but required in type '{ name: string; age: number; isLoggedIn: boolean; }'.

TypeScript is trying to tell us that isLoggedIn is a required property as per annotation but is not included in the initialization.

Let’s look at another scenario with a different type of error.

    person = {
      name: "John Doe",
      age: 25,
      sex: "m",
      isLoggedIn: boolean;
    };

    // Error: Type '{ name: string; age: number; sex: string; isEligible: boolean; }' is not assignable to type '{ name: string; age: number; isLoggedIn: boolean; }'.
    // Object literal may only specify known properties, and 'sex' does not exist in type '{ name: string; age: number; isLoggedIn: boolean; }'.

This second scenario tries to add a property that was not specified on the person type. TypeScript immediately flags an error saying that we are attempting to add a property, sex, to the person object.

Arrays

Arrays in JavaScript and other programming languages are a way of storing related elements in a single variable. We can annotate the type of a variable, function parameter and a return value to indicate that it is supposed to be an array.

// TypeScript syntax:
let names: type[] = [value1, value2,];
// An array of strings
let employees: string[] = ["John", "Jane", "Jack", "Joe"];
// An array of numbers
let fibonacci: number[] = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34];

Any attempt to include a value that is not of the specified type immediately causes TypeScript to throw a type error.

employees = ["John", "Joe", "Jane", false]; // Type 'boolean' is not assignable to type 'string'.

Essentially, the error is saying, “Hey! A boolean value cannot be included in an array that only expects strings.”

TypeScript Tuple

TypeScript has another array variation that can hold a fixed length of values, ordered with specific types called Tuple. Tuples are fixed in their length and types and are exclusive to TypeScript; they don’t exist in JavaScript.

// Creating a Tuple with its type definition
let person: [string, number];

person = ["John Doe", 123];

The person array declared above is referred to as a tuple with two elements. The first element is a string, and the second is a number.

Let’s look at some TypeScript errors that can occur if we don’t provide the correct value types.

person = [123, "John Doe"]; // TypeError type number is not assignable to type string. And 'type string is not assignable to type number.'

As we can see, it cares about not only the types in the length but also the order of those types.

Any attempt to add a third value to the person tuple also flags an error.

person = ["John Doe", 123, false]; // type [string, number, boolean ] is not assignable to type [string, number].

What happens when we try to access a method or property not present in a certain type?

console.log(person[1].subString()); // property subString does not exist on type ‘number’

TypeScript is aware that person[1] is of type number, and the number type has no property subString.

TypeScript Enums

Enum is short for enumerated types, and it does not exist in JavaScript; it is unique to TypeScript. It allows us to define a set of named constants that can be numeric or string values.

Numeric Enums

Numeric enums are number-based. They store string values as numbers.

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

TypeScript assigns numeric values starting at 0 to North, 1 to East and so on.

We can also manually assign values to our constants.

enum CardinalDirection {
  North = 12,
  East = 23,
  West = 29,
  South = 33,
}
console.log(CardinalDirection.North); // logs 12 to the console.
console.log(CardinalDirection[2]); // undefined

String Enums

String enums are similar to numeric enums except that they map string constants to string values. They are more readable and more often used than numeric enums. We can convert the numeric CardinalDirection enum above into a string enum like this:

enum CardinalDirection {
  North = "north",
  East = "east",
  West = "west",
  South = "south",
}
console.log(CardinalDirection.East); //logs east to the console.

Heterogeneous / Mixed Enums

Enums can also contain members or properties with mixed string and number values.

enum CardinalDirection {
  North = 1,
  East = "east",
  West = 3,
  South = "south",
} // Works fine but not advisable.

These kinds of enum are highly discouraged. Unless you have a strong reason, it is advised to refrain from using heterogeneous enums.

Also, any attempt to reference a constant that isn’t part of the enum will result in an error.

console.log(CardinalDirection.east); // Error
// property 'east' does not exist on type 'typeof CardinalDirection'. Did you mean 'East'?

TypeScript is smart enough to recognize that we have a typo and even help with suggestions.

The Union Type

A union type allows us to give a value multiple possible types using the single pipe (|) character. When you compose a union type from primitives, the union is exclusive.

// This variable can be a number or a string
let id: string | number;
id = "1"; // ok.
id = 1; // ok as well.
id = [0, 1]; //Error: Type 'number[]' is not assignable to type 'string | number'.

However, the union over object type is inclusive, meaning the object can match more than one member type in the union.

type Person = {
  name: string,
  age: number,
};
type SchoolMember = {
  major: string,
  courses: string[],
};

type Student = Person | SchoolMember;

let student: Student = {
  name: "Helen",
  age: 24,
  major: "Language Studies",
}; // This is ok. Typescript doesn't throw an error.

We can see from the above example that Student can have a type of Person or SchoolMember, and TypeScript is OK with it.

Type Narrowing With Union Types

Since the union type allows us to give multiple types to a value, type narrowing enables us to perform a type check before working with the value.

const formatPrice = (price: string | number) => {
  if (typeof price === "string") {
    // in this block, TypeScript knows price is of type string, and IntelliSense suggests properties and methods applicable to string types.
  } else {
    // at this point, TypeScript knows price is of type number, and IntelliSense suggests properties and methods applicable to number types.
  }
};

In the example above, we use typeof to check what type price is before performing our operations, and TypeScript is smart enough to know what we are working with.

The Intersection Type

TypeScript allows us to combine multiple types using an ampersand (&), which is particularly useful for object types.

type Person = {
  name: string,
  age: number,
};
type Lecturer = {
  specialization: string,
  yearOfExperience: number,
};

type Professor = Person & Lecturer;

let professor: Professor = {
  name: "John Doe",
  age: 42,
  specialization: "Plant Biology",
  yearOfExperience: 9,
};

In this example, we made a new type, Professor, that is just a direct intersection of Person and Lecturer.

When you omit a property, say yearOfExperience in type Lecturer, TypeScript will throw a type error with the following message:

Property 'yearOfExperience' is missing in type '{ name: string; age: number; specialization: string; }' but required in type 'Lecturer'.

Special Types

In TypeScript, types like any, never, null and undefined do not refer to any specific kind of data. Hence they are called special types.

Any

The any type is a way to opt in and out of type checking during the TypeScript compilation process allowing you to work with existing JavaScript codebases. A variable annotated to have the type any can accept values of all types. This type is not advisable as it tends to defeat the purpose of TypeScript.

let looseVariable: any;
looseVariable = 4;
looseVariable = "Hello, World";
looseVariable(); // doesn't throw an error

The type any can also be used as an argument or a return type.

const looseFunction = (arg: any): void => {
  // some logic goes here
};
looseFunction(55); // OK. No error.
looseFunction("I can also pass string here"); // This works too.
looseFunction({ one: "1", two: "2" }); // This works still.

The looseFunction has its argument type set to type any; hence any value of any type can be passed to it when it’s invoked.

Unknown

As mentioned above, the type any can accept values of any type, and it is not type-checked at compile time.

In certain circumstances, this sacrifice may be too expensive for your application. If we want a variable to take any value while still being type-checked at compile time, then the unknown type is our best bet. It is the type-safe counterpart of the type any.

let unknownValue: unknown;
unknownValue.toLowerCase(); // Error: Property 'toLowerCase' does not exist on type 'unknown'.
// If we set the type of unknownValue to 'any', Typescript wouldn't throw an error

Here TypeScript is saying the variable type is unknown; hence it can’t tell if the method toLowerCase exists on the variable. To get around this, we need to use a type guard to narrow down the type.

let unknownValue: unknown;
if (typeof unknownValue === "string") {
  // In this block, unknownValue is a string
  unknownValue.toLowerCase();
} else if (typeof unknownValue === "number") {
  //In this block, unknownValue is a number
  unknownValue.toFixed(2);
} else {
  // some other stuff can go here...
}

Void

The void type is commonly used as the return type for functions that don’t return anything. When used as the type of a variable, the void type almost always renders the variable useless as only undefined can be assigned to it. Let’s see the void type in action with the following examples.

let uselessVariable: void;

uselessVariable = undefined; // This works
uselessVariable = 0; // TypeError: Type 'number' is not assignable to type 'void'
uselessVariable = false; // TypeError: Type 'boolean' is not assignable to type 'void'.
uselessVariable = null; // This works only if strict mode is disabled.
uselessVariable = ""; //TypeError: Type 'string' is not assignable to type 'void'.

The void type is used mostly when annotating the return types of functions to say the function shouldn’t return any value.

function sayHello(message) {
  console.log(message);
}

In the example above, if you hover over the function’s name, you’ll see that TypeScript inferred the type for the return value to be void even without it being annotated. This is because, as we can see, the function doesn’t return anything, but we can also make it clear that it doesn’t return anything by annotating it.

function sayHello(message): void {
  console.log(message);
}

Null & Undefined

In TypeScript, null and undefined are values of type null and undefined, respectively, but they can also be subtypes of other types. Like the void type, they are less useful because they are only assignable to types any, unknown, and themselves.

let n: null = null;
let u: undefined = undefined;

// Nothing else can be assigned to them.
n = undefined; // TypeError. undefined not assignable to null
u = null; // TypeError. null is not assignable to undefined

Never

The never type denotes values that will never occur. It is only used when you are sure something will never occur. When used with variables, it signifies the variable will never be initialized or assigned a value.

let useLessVariable: never;

useLessVariable = null; // Error: Type 'null' is not assignable to type 'never'.
useLessVariable = undefined; // Error: Type 'undefined' is not assignable to type 'never'.
useLessVariable = false; // Error: Type 'boolean' is not assignable to type 'never'.

In the example above, TypeScript essentially says that the variable of type never type can never be initialized, or no value can ever be assigned to it.

When used as the return type of a function, it means the function should never return. The function is either going to be one that always throws an exception, so there’s no chance for anything to be returned or a function that continuously runs in some sort of loop.

let useLessVariable: never;
function throwError(errMsg: string): never {
  throw new Error(errMsg);
}

function infiniteLoop(): never {
  while (true) {
    console.log(
      "I'll never get past this block, hence will never return a value"
    );
  }
}

If we try to return something from the function infiniteLoop, TypeScript will complain because there’s not supposed to be anything ever returned from that function.

Type Inference

Until now, we have always explicitly specified the types of our variables. Another cool thing about TypeScript is that in some scenarios where the type of a variable is not stated, the TypeScript compiler can guess its type from certain values in your code smartly, and this is called type inference.

Let’s take some examples to see where TypeScript can infer the types of our variables:

1. When we declare and immediately initialize a variable

let counter = 1; // hover over the counter variable and Typescript would show something like "let counter:number"
counter = "some other stuff"; // TypeError: Type 'string' is not assignable to type 'number'.

Here, TypeScript can infer the type of the counter variable based on the value assigned to it on line 1. Hence we could still get all the IntelliSense support and type-checking feature for type number on our counter variable.

On the other hand, uninitialized variables would have the type any.

let anyType; // Hover over this variable and see the type is set to any.
anyType = 4; // This works.
anyType = "Some string"; // This works too.

2. Setting default values for parameters

Like initializing a variable explained above, TypeScript can also infer the type of a parameter with a default value.

function addTwoNumbers(a = 7, b = 8) {
  console.log(a + b);
}
addTwoNumbers("one", "two"); //Error. Argument of type 'string' is not assignable to parameter of type 'number'.

In the example above, even though we didn’t explicitly tell TypeScript the types of our parameters, it was able to figure it out from the default value assigned to them.

3. Determined return values

function addTwoNumbers(a: number, b: number) {
  let c = a + b;
  return c;
}

let result: number = addTwoNumbers(4, 6); // This works.

let result: string = addTwoNumbers(4, 6); //Error. Type 'number' is not assignable to type 'string'.

Because our function is just the sum of two number variables, TypeScript can infer that its return type is number. This makes it evident since adding numbers yields a number.

Type Aliases & Type Assertion

Type Alias

In TypeScript, a type alias assigns a new name to a given type. It doesn’t necessarily create a new type—it just provides a reference for the type. It is often used for reference types as aliasing primitives aren’t useful though it can be excellent for documentation. A type alias is created by preceding the name you want to give the type with the type keyword.

// A type alias
type customType = string | number | null;

let myVariable1: customType = 4; // This is fine.
let myVariable2: customType = false; // Error: Type 'boolean' is not assignable to type 'customType'.

// A type alias
type statusCode = 200 | 201 | 400;

let response1: statusCode = 400; // This is fine.
let response2: statusCode = 202; // Error: Type '202' is not assignable to type 'statusCode'.

Type aliases can be reused in our code and can even be exported to be used in other files or modules.

type customType = {
  name: string,
  age: number,
  married?: boolean,
};

A noticeable thing with our customType above is that the married property has a trailing question mark. Question marks are used to mark a property as optional. Hence any object annotated to be of type customType may or may not supply the married property.

// this type can be imported and reused in other files
export type customType = {
  name: string,
  age: number,
  married?: boolean,
};

let person1: customType = {
  name: "John Doe",
  age: 33,
  married: true,
}; // OK. All properties of customType are defined here.

let person2: customType = {
  age: 33,
  married: false,
}; // Error: Property 'name' is missing in type '{ age: number; married: false; }' but required in type 'customType'.

let person3: customType = {
  name: "John Doe",
  age: 33,
}; // OK. TypeScript isn’t sad because the married property was marked as optional.

Type Assertion

There are instances when you know more about a type than TypeScript does. One way to inform TypeScript of such type is using type assertion. Type assertion is a technique that tells TypeScript to treat a certain value as a certain type. It is purely effective at compile time and doesn’t affect the runtime behavior of our code.

Type assertion can be done in TypeScript in two ways: using the as keyword and using the angle bracket (<>) notation.

Using the as keyword

Let’s assume we want to get a canvas element in our document.

let canvas = document.querySelector("#canvas-element");

At this point, all TypeScript knows about the canvas variable is some HTML element. It doesn’t know if it is an input, a div, a canvas or a paragraph element. But when we write:

let canvas = document.querySelector("#canvas-element") as HTMLCanvasElement;

Here, TypeScript knows that the canvas variable isn’t just an HTML element but a canvas element.

Another example is:

let myVariable: unknown;
myVariable = "This is a string";

console.log(myVariable.length); //Error: Property 'length' does not exist on type 'unknown'.
console.log((myVariable as string).length); // prints 16 to the console.

Using the angle bracket <> notation

The angle bracket notation version of type assertion works exactly like using the as syntax, but when using TypeScript with JSX, only the as keyword can be used for assertion.

So, let’s convert the example above to use the bracket notation:

let myVariable: unknown;
myVariable = "This is a string";

console.log(myVariable.length); //Error. Property 'length' does not exist on type 'unknown'.
console.log((<string>myVariable).length); // prints 16 to the console.

On line 4, we convert myVariable from unknown type to string type, which is why we could access the length property without any error from TypeScript.

Functions

Defining and using functions is one of the basic tasks in any programming language, and it’s no different with TypeScript. Functions are a way to group a set of related code into a logical block that can be reused.

TypeScript fully supports the existing syntax for functions in JavaScript and adds type information as its additional feature. The type information provides documentation and helps reduce the likelihood of bugs as we can’t pass or return incorrect data (types) from a type-safe function.

A typed function has the following syntax:

function functionName(optionalArg: type): returnType {
  // some logic here
}

//ex. 1
function greet(): void {
  console.log("hello, world");
} //accepts no argument and doesn’t return a value.

//ex. 2 creating a function with typed args and typed return value
function add(a: number, b: number): number {
  return a + b;
} // accepts two arguement and returns the result of their addiction.

TypeScript ensures that the add function receives exactly two arguments and must return a value of type number. TypeScript would throw an error in any attempt to introduce a new argument or return a type other than number.

function add(a: number, b: number): number {
  return a + b;
}

let result: string = adder(43, 59); // Error: Type 'number' is not assignable to type 'string'.
add(23, 43); // returns 66
add(23, 43, 14); // Error: Expected 2 arguments, but got 3.
add("one", "two"); //Error: Argument of type 'string' is not assignable to parameter of type 'number'.

It’s also important to add that this works with all types of functions (arrow functions, anonymous functions), not just named functions.

const add = (a: number, b: number): number => {
  return a + b;
};

Optional Parameters

The parameters defined in a function can be marked as optional or given a default value, but not both. A question mark precedes the name of optional parameters. Also, optional parameters are defined first, followed by required parameters. A required parameter cannot follow an optional parameter.

function add(a?: number, b: number): number {
  if (a) {
    return a + b;
  } else return b;
} // Error: A required parameter cannot follow an optional parameter.

// The correct version of the function would be:
function add(b: number, a?: number): number {
  if (a) {
    return a + b;
  } else {
    return b;
  }
} // This works, and TypeScript is happy.

let result = add(5, 8); // result is 13.
let result2 = add(5); // result is 5.

Default Values

We can assign typed default values to function parameters to be used when a value is not passed as an argument to the parameter by the function caller.

function computeTax(income: number, rate = 0.1): number {
  return income * rate;
} // rate will default to 0.1 if the caller doesn't specify

let netIncome1 = computeTax(4500); // rate is 0.1 hence computeTax returns 450
let netIncome2 = computeTax(45000, 0.2); // rate is 0.2 hence computeTax returns 225

Classes

Functionalities can be built into classes and objects created from those classes. JavaScript, until ES2015, had no such thing as classes. As with other features, TypeScript allows us to add types to the members of a class.

A simple class declaration looks like this:

class Student {
  studentName: string;
  studentId: number;

  constructor(id: number, name: string) {
    this.studentId = id;
    this.studentName = name;
  }

  getStudentDetails(): string {
    return `This is ${this.studentName} with the student ID of ${this.studentId}`;
  }
}

The studentName and studentId are properties, while getStudentDetails is a method of the class. The constructor method gets called when an instance of the class is created. The constructor works very much the same as regular functions. They can have type annotations and default values. Let’s instantiate an object from the class defined above:

let myStudent = new Student(23, "Obi Madu");

The line of code above creates an object named myStudent from the Student class. The two arguments supplied are what gets passed to the id and name parameters defined on the constructor function.

Let’s see what happens when we create an object with fewer or more arguments than the constructor expects.

let myStudent = new Student(); // Error: constructor Student(id: number, name: string): Student expected 2 arguments, but got 0.

let myStudent = new Student(23, "Obi Madu", "Economics"); // Error: Expected 2 arguments, but got 3.

let myStudent = new Student("Obi Madu", 23); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

There are instances where we want to prevent some class members from being accidentally modified after they’ve been initialized in the constructor function.

To do that with TypeScript, we can precede such members with the readonly keyword. This is not a JavaScript keyword—it is a modifier we use in TypeScript to mark certain properties in classes, objects, etc. It is very handy to prevent accidental modification.

class Student {
  readonly name: string = "John Doe";
  age: number;
  constructor(name, age) {
    this.name = name; // modification is allowed here
    this.age = age;
  }
  randomMethod() {
    this.name = "John Kenedy"; // Error: Cannot assign to 'name' because it is a read-only property.
  }
}
const myStudent = new Student("Obi Madu", 25);
myStudent.name = "John Mark"; // Error: Cannot assign to 'name' because it is a read-only property.

TypeScript flags an error on lines 9 and 13, saying that you cannot assign to name because it is a read-only property.

Interfaces

Interfaces are used to create reusable types that describe the shape of objects. They are very similar to type aliases, but a key difference between them is that interfaces can only be used with objects, not primitives or type literals. If an object implements an interface, it must define all variables and implement all methods declared on the interface.

// An interface
interface IStudent {
  first: string;
  last: string;
  sayHello: () => string;
}

const student: IStudent = {
  first: "Ifeoma",
  last: "Imoh",
  sayHello() {
    return "Hello";
  },
};

In the example above, notice that, unlike when defining a type alias, when defining an interface, there is no equals sign (=) after the name of the interface. If any of the properties or methods defined in the IStudent interface were not declared or implemented in the student object, then TypeScript would flag an error except if the property was marked optional.

If you know beforehand the structure or shape of an object, you may want to declare an interface for that and make your object implement that interface. By doing so, you prevent any form of error or omission.

Generics

In TypeScript, generics allow us to define interfaces, functions and classes that are flexible and can work with multiple types, avoiding code duplication where necessary. For instance, we need to write a function that can either take a string, number or boolean argument and return it to the caller. Ordinarily, we would need to write three different functions, one for each.

function printString(arg: string): string {
  return arg;
}
function printNumber(arg: number): number {
  return arg;
}
function printBoolean(arg: boolean): boolean {
  return arg;
}

Obvious code duplication. What if we were to define the same function for other types like tuples, objects and arrays? We’d keep duplicating the function by creating separate functions with different names. That’s where generics come to the rescue. Let’s rewrite the functions above using a generic type.

function printValue<T>(arg: T): T {
  return arg;
}

This function accepts an argument, and it returns that argument. We customized the function to work with different types, so the input type of this function is what will be the return type as well.

You may have noticed the <T>, known as the Type Parameter. Type parameters are placed inside an opening and closing angle bracket immediately following the function name. Although the type parameter can be anything, the uppercase letter T is conventionally used.

To call a generic function like our example above, we would write:

function printValue<T>(arg: T): T {
  return arg;
}
printValue < string > "This is a string";
printValue < number > 342;
printValue < boolean > true;

Generic Classes

In the case of generic classes, the type parameter is placed immediately after the class name. A generic class may contain generic fields and generic methods.

For instance, we have a Student class with a name field of type string and an id that can either be a string or a number.

class Student<T, U> {
  private id: T;
  protected name: U;
  constructor(id: T, name: U) {
    this.id = id;
    this.name = name;
  }
  displayInfo() {
    console.log("id is ", this.id, "and name is ", this.name);
  }
}
 
let std1 = new Student<number, string>(101, "John Doe");
std1.displayInfo(); // prints "id is 101 and name is John Doe".
let std2 = new Student<string, string>("202", "Bruce Wayne");
std2.displayInfo(); // prints "id is 202 and name is Bruce Wayne".

From the example above, we can have more than one type parameter in our generics, and they only need to be comma separated in the angle bracket. Each type parameter is a placeholder for the actual type that would be passed when the class is instantiated.

Generic Interfaces

As with classes and functions, you can also use generics with interfaces. Interfaces, as discussed earlier, only help define the shape of an object. The compiler only uses them for type-checking, strips them off afterward, and they never make it to runtime. For example:

interface Person<T, U> {
  name: T;
  id: U;
}

let john: Person<string, string> = {
  name: "John Doe",
  id: "301", // OK. string passed to id here.
};

let bruce: Person<string, number> = {
  name: "Bruce Wayne",
  id: 301, // OK.  number passed to id here.
};

Conclusion

While there is more to learn about TypeScript, this article exposes you to what you need to know to get started writing TypeScript. For more on TypeScript, you can check out the official documentation. To learn about using TypeScript with React, read this post


Ifeoma-Imoh
About the Author

Ifeoma Imoh

Ifeoma Imoh is a software developer and technical writer who is in love with all things JavaScript. Find her on Twitter or YouTube.

Related Posts

Comments

Comments are disabled in preview mode.