Full Stack Developer, Tech Geek, Audiophile, Cinephile, and Lifelong Learner!

Short Handbook for TypeScript types, interfaces and generics

S

TypeScript is a programming language that extends JavaScript by adding types. Types are annotations that describe the kind of data that a variable can hold, such as numbers, strings, booleans, arrays, objects, functions, etc.

In this post let’s explore the noteworthy ways to declare and use types, functions, interfaces, generics, and classes in TypeScript.

Benifits of Using Types

You might be wondering why bother with types if JavaScript works fine without them, after all!

With TypeScript, you can avoid common errors like typos, null pointer exceptions, or incorrect function calls. You can also refactor your code with confidence, knowing that any changes will be checked by the compiler and flagged if they break something. TypeScript also helps you write more expressive and concise code by providing features like interfaces, generics, unions, intersections, and more. These features allow you to define and manipulate types in powerful ways that are not possible in plain JavaScript. In short, TypeScript makes your life easier as a developer by making your code safer, cleaner, and smarter.

How to Use Basic Types

For TypeScript, there are several built-in types that you can use to define variables, functions, and objects. Here are some examples of TypeScript types and how to use them:

// boolean type
let isDone: boolean = false;

// number types (integer and floating-point)
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

// string type
let color: string = "blue";
let fullName: string = `Bob Bobbington`;
let sentence: string = `Hello, my name is ${fullName}.`;

// array type
let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];

// tuple type
let x: [string, number];
x = ["hello", 10];

// enum type
enum Color {
  Red,
  Green,
  Blue,
}
let c: Color = Color.Green;

// any type
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;

// void type (function that returns no value)
function warnUser(): void {
  console.log("This is my warning message");
}

// null and undefined types
let u: undefined = undefined;
let n: null = null;

How to Use Functions

The followings are the ways to declare and use functions:

// 1. Function with typed parameters and return type 
function addNumbers(num1: number, num2: number): number {
    return num1 + num2;
}

// 2. Function with optional parameter & default parameter
function greetUser(name?: string): void {
    console.log(`Hello, ${name ?? 'world'}`);
}

function repeatText(text: string, times: number = 3): string {
    return text.repeat(times);
}
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}

let name1 = buildName("Bob"); // name1 is "Bob"
let name2 = buildName("Bob", "Adams"); // name2 is "Bob Adams"

function buildName2(firstName: string, lastName = "Smith"): string {
  return firstName + " " + lastName;
}

let name3 = buildName2("Bob"); // name3 is "Bob Smith"
let name4 = buildName2("Bob", "Adams"); // name4 is "Bob Adams"


// 4. Function with rest parameter
function sumNumbers(...numbers: number[]): number {
    return numbers.reduce((total, number) => total + number, 0);
}

function buildName(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}

let name1 = buildName("Bob", "Adams"); // name1 is "Bob Adams"
let name2 = buildName("Bob", "Adams", "Sr."); // name2 is "Bob Adams Sr."


// 5. Function with overloaded signatures
function convertValue(value: string): number;
function convertValue(value: number): string;
function convertValue(value: string | number): string | number {
    if (typeof value === 'string') {
        return parseInt(value, 10);
    } else {
        return value.toString();
    }
}

// 6. Function expression
let add = function(x: number, y: number): number {
  return x + y;
}

How to Use Interfaces

Interfaces are used to define the structure of an object in TypeScript. Here are some ways to declare and use interfaces in TypeScript:

// 1. Object Interface:
interface Animal {
  name: string;
  legs: number;
  eatsMeat: boolean;
}

function feed(animal: Animal) {
  console.log(`${animal.name} is being fed.`);
}

let lion: Animal = { name: "Lion", legs: 4, eatsMeat: true };
let snake: Animal = { name: "Snake", legs: 0, eatsMeat: true };
feed(lion); // prints "Lion is being fed."
feed(snake); // prints "Snake is being fed."

// 2. Optional Properties
interface Car {
  make: string;
  model?: string;
  year: number;
}

function describeCar(car: Car) {
  console.log(`This is a ${car.make} ${car.model ?? "car"} made in ${car.year}.`);
}

let honda: Car = { make: "Honda", year: 2021 };
let ford: Car = { make: "Ford", model: "Mustang", year: 2022 };
describeCar(honda); // prints "This is a Honda car made in 2021."
describeCar(ford); // prints "This is a Ford Mustang made in 2022."

// 3. Readonly Properties
interface Circle {
  readonly radius: number;
  area: () => number;
}

class CircleImpl implements Circle {
  readonly radius: number;
  constructor(radius: number) {
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius * this.radius;
  }
}

let circle = new CircleImpl(5);
console.log(circle.radius); // prints 5
console.log(circle.area()); // prints 78.53981633974483
circle.radius = 10; // error: cannot assign to 'radius' because it is a read-only property

// 4. Indexable Properties
interface Dictionary {
  [key: string]: string;
}

let dict: Dictionary = {};
dict["hello"] = "world";
dict["goodbye"] = "world";
console.log(dict); // prints { hello: "world", goodbye: "world" }

Usage of Generics

Generics are a powerful feature of TypeScript that allow us to write functions and classes that can work with a variety of types, without knowing the specific type in advance. Here’s how to declare and use generics in TypeScript, with some detailed examples:

Function Generics: We can declare a function that takes a generic type T by putting it in angle brackets <> right after the function name. We can then use this type T in the function parameters, return type, and body.

function repeat<T>(item: T, times: number): T[] {
  let result = [];
  for (let i = 0; i < times; i++) {
    result.push(item);
  }
  return result;
}

let words = repeat<string>("hello", 3);
console.log(words); // prints ["hello", "hello", "hello"]

let numbers = repeat<number>(5, 4);
console.log(numbers); // prints [5, 5, 5, 5]

Above example, we declare a function repeat that takes a generic type T and an integer times. Inside the function, we create an empty array result and loop times times, pushing the item parameter onto the array each time. We then return the result array. We call the repeat function twice, once with a string "hello" and times set to 3, and again with the number 5 and times set to 4.

Class Generics: We can also declare a class that takes a generic type T by putting it in angle brackets <> after the class name. We can then use this type T as a type parameter for the class properties and methods.

class Stack<T> {
  private items: T[] = [];
  push(item: T) {
    this.items.push(item);
  }
  pop(): T | undefined {
    return this.items.pop();
  }
}

let stack = new Stack<number>();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.pop()); // prints 3
console.log(stack.pop()); // prints 2

let words = new Stack<string>();
words.push("hello");
words.push("world");
console.log(words.pop()); // prints "world"

Here, we declare a class Stack that takes a generic type T. Inside the class, we declare a private property items of type T[] and two methods push and pop that add and remove items from the stack. We create two instances of the Stack class, one with a number type parameter and another with a string type parameter.

Generic Constraints: We can also add constraints to our generic types, so that they can only be of certain types or implement certain interfaces.

interface Length {
  length: number;
}

function printLength<T extends Length>(arg: T): void {
  console.log(`Length: ${arg.length}`);
}

let arr = [1, 2, 3];
let str = "hello";
let obj = { length: 5 };
printLength(arr); // prints "Length: 3"
printLength(str); // prints "Length: 5"
printLength(obj); // prints "Length: 5"
printLength(123); // error: Argument of type '123' is not assignable to parameter of type 'Length'.

For generic constrain, we define an interface Length that requires objects to have a length property of type number. We then define a function printLength that takes an argument of type T, which extends the Length interface. The function logs the length of the input object. We create three variables arr, str, and obj, all of which have a length property. We call the printLength function with each variable, and it prints out the length of the objects. We attempt to call the function with a value of 123, which results in an error because it does not have a length property.

Type Assertions

Type Assertion in TypeScript is a way to tell the TypeScript compiler to treat a value as a specific type, even if TypeScript’s type inference does not recognize it as such. Here are some examples of how to use type assertion in TypeScript:

// Using 'as' keyword
let someValue: any = "hello world";
let strLength: number = (someValue as string).length;
console.log(strLength); // prints 11

// Using angle-bracket syntax
let anotherValue: any = "foo bar";
let strLength2: number = (<string>anotherValue).length;
console.log(strLength2); // prints 7

// Type Assertion on Functions
function sayHello(name: string | undefined): void {
  console.log(`Hello, ${name!.toUpperCase()}!`);
}

sayHello("John"); // prints "Hello, JOHN!"
sayHello(undefined); // throws an error: Cannot read property 'toUpperCase' of undefined

let name: string | undefined = "Sarah";
sayHello(name as string); // prints "Hello, SARAH!"

First example, we define a variable someValue with a type of any. We then use the as keyword to tell TypeScript that someValue is actually a string. We assign the length of the string to a variable strLength of type number. We then log strLength to the console, which prints out the length of the string.

Next example, we define a variable anotherValue with a type of any. We use the angle-bracket syntax to tell TypeScript that anotherValue is a string. We then assign the length of the string to strLength2, and log it to the console.

And the third example, we define a function sayHello that takes a parameter name of type string or undefined. We use the ! operator to assert that name is not null or undefined. We then use the toUpperCase() method to convert the name to uppercase and log it to the console. We call the function with a string value of "John", and with undefined, which throws an error because we cannot call toUpperCase() on undefined.

We also define a variable name of type string or undefined, and then use type assertion with the as keyword to call sayHello with name. This allows us to call the function with a value that is potentially undefined.

Use of Classes

Classes provide a way to define blueprints for creating objects with specific properties and methods allowing us to utilize Encapsulation, Inheritance, Polymorphism and Type Safety in a maintainable manner. The followings are the ways to write classes in TypeScript:

// declare a class with constructor and methods
class Person {
    constructor(public firstName: string, public lastName: string, public age: number) { }

    getFullName(): string {
        return `${this.firstName} ${this.lastName}`;
    }
}


// class inheritance
class Student extends Person {
    constructor(firstName: string, lastName: string, age: number, public studentId: number) {
        super(firstName, lastName, age);
    }

    getStudentInfo(): string {
        return `${this.getFullName()}, Age: ${this.age}, Student ID: ${this.studentId}`;
    }
}

// access modifiers for class members
class Teacher extends Person {
    private salary: number;

    constructor(firstName: string, lastName: string, age: number, salary: number) {
        super(firstName, lastName, age);
        this.salary = salary;
    }

    getSalary(): number {
        return this. Salary;
    }
}

Now, we can summarize like there are several reasons why you might want to use TypeScript instead of JavaScript for your next project:
  1. Type safety: TypeScript is a statically typed language, which means that it checks types at compile time. This helps catch type-related errors before the code is executed and can improve the overall reliability and maintainability of your code.
  2. Code maintainability: TypeScript supports features such as interfaces and classes that can help make your code more organized and easier to maintain as it grows in size and complexity.
  3. Tooling support: TypeScript is widely supported by development tools such as Visual Studio Code, which provides features such as code completion and error highlighting that can help make your development process more efficient.
  4. Improved developer productivity: TypeScript provides features such as type inference and enhanced code completion that can help you write code faster and with fewer errors.
  5. Compatibility with JavaScript: TypeScript is a superset of JavaScript, which means that you can use all of your existing JavaScript code with TypeScript. This can help you gradually migrate your codebase to TypeScript without having to rewrite everything from scratch.

Overall, repeating again, TypeScript can help you write more dependable, maintainable, and productive code, especially for larger and more complex projects.

Share on:

Add Comment

  |     |  
Full Stack Developer, Tech Geek, Audiophile, Cinephile, and Lifelong Learner!