Interview

30 TypeScript Interview Questions and Answers

Prepare for your next interview with this guide on TypeScript, featuring common questions and answers to enhance your understanding and skills.

TypeScript has emerged as a powerful tool for modern web development, offering a statically typed superset of JavaScript that enhances code quality and maintainability. By providing optional static typing, TypeScript helps developers catch errors early in the development process, leading to more robust and scalable applications. Its compatibility with existing JavaScript code and extensive tooling support make it a valuable addition to any developer’s skill set.

This article presents a curated selection of TypeScript interview questions designed to test and expand your understanding of the language. By working through these questions and their detailed answers, you will be better prepared to demonstrate your proficiency in TypeScript and tackle the challenges posed in technical interviews.

TypeScript Interview Questions and Answers

1. What is TypeScript and why would you use it over JavaScript?

TypeScript is an open-source programming language developed by Microsoft. It is a strict syntactical superset of JavaScript, meaning any valid JavaScript code is also valid TypeScript code. The primary feature TypeScript introduces is static typing, allowing developers to define types for variables, function parameters, and return values. This helps catch errors at compile time, making development more robust and less error-prone.

There are several reasons to use TypeScript over JavaScript:

  • Static Typing: TypeScript’s type system allows for early detection of errors, saving time and reducing bugs.
  • Enhanced IDE Support: TypeScript provides better autocompletion, navigation, and refactoring capabilities in modern IDEs, improving productivity.
  • Improved Readability and Maintainability: Type annotations make the code more readable and easier to understand, especially in large codebases.
  • Advanced Features: TypeScript includes features like interfaces, enums, and generics, which are not available in plain JavaScript.
  • Compatibility: TypeScript is fully compatible with existing JavaScript libraries and frameworks, making it easy to integrate into existing projects.

2. How do you declare a variable with a specific type?

In TypeScript, you declare a variable with a specific type by using a colon followed by the type after the variable name. This helps in catching type-related errors during development and provides better code documentation.

Example:

let age: number = 30;
let name: string = "John";
let isStudent: boolean = true;

In the example above, age is declared as a number, name as a string, and isStudent as a boolean. This ensures that these variables can only hold values of their specified types, and any attempt to assign a value of a different type will result in a compile-time error.

3. How do you annotate a function parameter and return type?

In TypeScript, you can annotate function parameters and return types to ensure type safety and catch potential errors during development. This is done using a colon followed by the type after the parameter name and after the function’s closing parenthesis for the return type.

Example:

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

In this example, the function add takes two parameters a and b, both of which are annotated as number. The return type of the function is also annotated as number. This ensures that the function only accepts numbers as arguments and returns a number.

4. How do you define an interface and implement it in a class?

In TypeScript, an interface is used to define the structure of an object. It specifies the properties and methods that an object should have. When a class implements an interface, it must provide the implementation for all the properties and methods defined in the interface.

Example:

interface Person {
    name: string;
    age: number;
    greet(): void;
}

class Student implements Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet(): void {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
}

const student = new Student('John Doe', 20);
student.greet();

5. How do you create a class with properties and methods?

In TypeScript, a class is a blueprint for creating objects with specific properties and methods. Classes can include properties, constructors, and methods to define the behavior of the objects created from the class.

Example:

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet(): string {
        return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
    }
}

const person1 = new Person('Alice', 30);
console.log(person1.greet());

6. How do you use public, private, and protected access modifiers in a class?

In TypeScript, access modifiers control the visibility of class members. The three main access modifiers are:

  • public: Members are accessible from anywhere.
  • private: Members are accessible only within the class they are defined.
  • protected: Members are accessible within the class they are defined and in derived classes.

Here is a concise example to illustrate their usage:

class Animal {
    public name: string;
    private age: number;
    protected species: string;

    constructor(name: string, age: number, species: string) {
        this.name = name;
        this.age = age;
        this.species = species;
    }

    public getAge(): number {
        return this.age;
    }
}

class Dog extends Animal {
    constructor(name: string, age: number, species: string) {
        super(name, age, species);
    }

    public getSpecies(): string {
        return this.species;
    }
}

const dog = new Dog('Buddy', 5, 'Canine');
console.log(dog.name); // Accessible
console.log(dog.getAge()); // Accessible via public method
console.log(dog.getSpecies()); // Accessible via public method
// console.log(dog.age); // Error: 'age' is private
// console.log(dog.species); // Error: 'species' is protected

7. How do you implement inheritance between two classes?

Inheritance in TypeScript allows a class to inherit properties and methods from another class. This is achieved using the extends keyword. The child class can access and override properties and methods of the parent class. Additionally, the super function is used to call the constructor of the parent class.

Example:

class Animal {
    name: string;

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

    makeSound(): void {
        console.log("Some generic sound");
    }
}

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }

    makeSound(): void {
        console.log("Bark");
    }
}

const myDog = new Dog("Buddy");
myDog.makeSound(); // Output: Bark

In this example, the Dog class inherits from the Animal class. The Dog class overrides the makeSound method to provide a specific implementation. The super function is used in the Dog constructor to call the Animal constructor.

8. How do you create a generic function or class?

Generics in TypeScript allow you to create reusable components that can work with a variety of data types. They provide a way to create functions and classes that can operate on different types while maintaining type safety.

Here is an example of a generic function:

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

let output1 = identity<string>("Hello");
let output2 = identity<number>(42);

In this example, the function identity takes a type parameter T and an argument of type T, and returns a value of type T. This allows the function to be used with different types while ensuring type safety.

Similarly, you can create a generic class:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;

In this example, the class GenericNumber uses a type parameter T to define properties and methods that can operate on different types.

9. How do you define and use an enum?

Enums in TypeScript are a way to define a set of named constants. They can be used to create a collection of related values that can be either numeric or string-based.

Here is an example of how to define and use an enum in TypeScript:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

function move(direction: Direction) {
    switch (direction) {
        case Direction.Up:
            console.log("Moving up");
            break;
        case Direction.Down:
            console.log("Moving down");
            break;
        case Direction.Left:
            console.log("Moving left");
            break;
        case Direction.Right:
            console.log("Moving right");
            break;
    }
}

move(Direction.Up); // Output: Moving up

In this example, the Direction enum is defined with four possible values: Up, Down, Left, and Right. The move function takes a Direction as an argument and uses a switch statement to determine the action to take based on the direction.

10. How do you use union types in a function parameter?

Union types in TypeScript allow you to specify that a variable or a parameter can hold more than one type. This is particularly useful when you want a function to accept different types of arguments while still maintaining type safety.

Example:

function printId(id: number | string) {
    if (typeof id === "string") {
        console.log(`ID: ${id.toUpperCase()}`);
    } else {
        console.log(`ID: ${id}`);
    }
}

printId(101); // ID: 101
printId("abc"); // ID: ABC

In this example, the printId function can accept either a number or a string as its parameter. The function then uses a type guard (typeof) to handle each type appropriately.

11. How do you use intersection types in a function parameter?

Intersection types in TypeScript are used to combine multiple types into a single type. This is useful when you want to ensure that a function parameter meets multiple type constraints. Intersection types are denoted using the & operator.

Example:

type Person = {
    name: string;
    age: number;
};

type Employee = {
    employeeId: number;
    department: string;
};

type PersonEmployee = Person & Employee;

function printEmployeeDetails(employee: PersonEmployee) {
    console.log(`Name: ${employee.name}`);
    console.log(`Age: ${employee.age}`);
    console.log(`Employee ID: ${employee.employeeId}`);
    console.log(`Department: ${employee.department}`);
}

const employee: PersonEmployee = {
    name: "John Doe",
    age: 30,
    employeeId: 12345,
    department: "Engineering"
};

printEmployeeDetails(employee);

12. How do you create and use a type alias?

In TypeScript, a type alias is a way to create a new name for a type. This can be particularly useful for simplifying complex type definitions or for making code more readable and maintainable. Type aliases can be used for primitive types, object types, union types, and more.

Example:

type Point = {
    x: number;
    y: number;
};

function printPoint(point: Point): void {
    console.log(`x: ${point.x}, y: ${point.y}`);
}

const myPoint: Point = { x: 10, y: 20 };
printPoint(myPoint);

In this example, the type alias Point is created to represent an object with x and y properties, both of which are numbers. The printPoint function then uses this type alias to specify the type of its parameter, making the code more readable and easier to understand.

13. How do you overload a function?

Function overloading in TypeScript allows you to define multiple signatures for a function, enabling it to handle different types or numbers of arguments. Unlike languages like C++ or Java, TypeScript does not support true function overloading at runtime. Instead, it uses a combination of multiple function signatures and a single implementation to achieve similar functionality.

Example:

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

console.log(add(1, 2)); // 3
console.log(add("Hello, ", "world!")); // Hello, world!

In this example, the add function is overloaded with two different signatures: one for adding numbers and another for concatenating strings. The actual implementation of the function uses a single definition that handles both cases.

14. How do you create and use mapped types?

Mapped types in TypeScript are a powerful feature that allows you to create new types by transforming properties of existing types. They are often used to create variations of types, such as making all properties optional, readonly, or even applying transformations to the types of the properties.

Example:

type Person = {
    name: string;
    age: number;
    address: string;
};

// Making all properties optional
type PartialPerson = {
    [P in keyof Person]?: Person[P];
};

// Making all properties readonly
type ReadonlyPerson = {
    readonly [P in keyof Person]: Person[P];
};

// Transforming property types
type StringifiedPerson = {
    [P in keyof Person]: string;
};

In the example above, PartialPerson is a mapped type that makes all properties of Person optional. ReadonlyPerson makes all properties readonly, and StringifiedPerson transforms all property types to string.

15. How do you use conditional types in a function?

Conditional types in TypeScript allow you to define types based on a condition. They follow the syntax T extends U ? X : Y, where T is the type to check, U is the condition, X is the type if the condition is true, and Y is the type if the condition is false.

Example:

type IsString<T> = T extends string ? "Yes" : "No";

function checkType<T>(value: T): IsString<T> {
    return (typeof value === "string" ? "Yes" : "No") as IsString<T>;
}

console.log(checkType("Hello")); // Output: Yes
console.log(checkType(123));     // Output: No

In this example, the IsString type checks if a given type T extends string. If it does, it returns the type "Yes", otherwise it returns the type "No". The checkType function uses this conditional type to determine the type of the input value and returns the appropriate string.

16. How do you implement discriminated unions in a function?

Discriminated unions in TypeScript are a way to create a type that can represent multiple different types of values, each with a unique “discriminant” property that makes it easy to determine which type of value you are dealing with.

Here is an example of how to implement discriminated unions in a function:

type Circle = {
    kind: "circle";
    radius: number;
};

type Square = {
    kind: "square";
    sideLength: number;
};

type Shape = Circle | Square;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius * shape.radius;
        case "square":
            return shape.sideLength * shape.sideLength;
        default:
            throw new Error("Unknown shape");
    }
}

const myCircle: Circle = { kind: "circle", radius: 5 };
const mySquare: Square = { kind: "square", sideLength: 10 };

console.log(getArea(myCircle)); // 78.53981633974483
console.log(getArea(mySquare)); // 100

In this example, the Shape type is a discriminated union of Circle and Square. Each type has a kind property that acts as the discriminant. The getArea function uses a switch statement to handle each type of shape based on the value of the kind property.

17. How do you use the keyof operator in a type definition?

The keyof operator in TypeScript is used to create a union type of all the keys of a given object type. This is useful for scenarios where you need to ensure that a value is one of the keys of an object, providing type safety and reducing potential errors.

Example:

type Person = {
    name: string;
    age: number;
    location: string;
};

type PersonKeys = keyof Person; // 'name' | 'age' | 'location'

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person: Person = {
    name: 'John',
    age: 30,
    location: 'New York'
};

const name = getProperty(person, 'name'); // 'John'
const age = getProperty(person, 'age'); // 30

In this example, the keyof operator is used to create a type PersonKeys that represents the keys of the Person type. The getProperty function then uses this type to ensure that the key passed to it is one of the keys of the object.

18. How do you use indexed access types?

Indexed access types in TypeScript are used to retrieve the type of a specific property within an object type. This is done using the syntax T[K], where T is the type and K is the key.

Example:

type Person = {
    name: string;
    age: number;
    location: string;
};

type NameType = Person['name']; // string
type AgeType = Person['age']; // number
type LocationType = Person['location']; // string

In this example, NameType, AgeType, and LocationType are types that correspond to the types of the name, age, and location properties of the Person type, respectively.

Indexed access types can also be used in more complex scenarios, such as when working with generics:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person: Person = { name: 'Alice', age: 30, location: 'Wonderland' };
const name: NameType = getProperty(person, 'name'); // Alice
const age: AgeType = getProperty(person, 'age'); // 30

In this example, the getProperty function uses indexed access types to ensure that the type of the returned value matches the type of the specified property.

19. How do you define and use tuple types?

In TypeScript, a tuple is a special type of array that allows you to specify the types of elements at specific positions. Unlike regular arrays, where the type of each element is not fixed, tuples enforce a fixed structure and type for each element.

Example:

// Defining a tuple type
let person: [string, number];

// Initializing the tuple
person = ["Alice", 30];

// Accessing tuple elements
let name: string = person[0]; // "Alice"
let age: number = person[1];  // 30

In the example above, the tuple person is defined to have a string as its first element and a number as its second element. This ensures that the types are consistent and predictable.

20. How do you use the readonly modifier in an interface or class?

The readonly modifier in TypeScript is used to ensure that a property cannot be reassigned after it has been initialized. This is particularly useful for defining constants or ensuring that certain properties remain immutable.

In an interface, the readonly modifier is used as follows:

interface User {
    readonly id: number;
    name: string;
}

let user: User = { id: 1, name: "John Doe" };
user.name = "Jane Doe"; // This is allowed
user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

In a class, the readonly modifier can be used similarly:

class User {
    readonly id: number;
    name: string;

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

let user = new User(1, "John Doe");
user.name = "Jane Doe"; // This is allowed
user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

21. How do you define optional properties in an interface?

In TypeScript, interfaces are used to define the structure of an object. Optional properties in an interface are properties that may or may not be present in the object. These properties are defined using a question mark (?) after the property name.

Example:

interface User {
    name: string;
    age?: number;
    email?: string;
}

const user1: User = {
    name: "Alice"
};

const user2: User = {
    name: "Bob",
    age: 25
};

In the above example, the User interface has three properties: name, age, and email. The age and email properties are optional, meaning that objects conforming to the User interface may or may not include them.

22. How do you handle null and undefined types?

In TypeScript, null and undefined are distinct types that represent the absence of a value. Handling these types effectively is important to avoid runtime errors and ensure type safety.

TypeScript provides several ways to handle null and undefined:

  • Union Types: You can use union types to explicitly specify that a variable can be of a certain type or null/undefined.
  • Optional Chaining: This allows you to safely access deeply nested properties without having to check for null or undefined at each level.
  • Nullish Coalescing: This operator provides a way to fall back to a default value when dealing with null or undefined.

Example:

function greet(name: string | null | undefined): string {
    return `Hello, ${name ?? 'Guest'}!`;
}

let userName: string | null = null;
console.log(greet(userName)); // Output: Hello, Guest!

userName = "Alice";
console.log(greet(userName)); // Output: Hello, Alice!

In this example, the greet function can handle a name that is either a string, null, or undefined. The nullish coalescing operator (??) is used to provide a default value of ‘Guest’ when name is null or undefined.

23. How do you use the never type in a function?

The never type in TypeScript is a type that represents the type of values that never occur. It is often used in functions that are expected to never return a value. This can be useful for functions that always throw an error or have infinite loops.

Example:

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

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

In the first example, the function throwError is designed to always throw an error, and thus it never returns a value. The return type is specified as never to indicate this behavior. In the second example, the function infiniteLoop contains an infinite loop and will never return, so its return type is also never.

24. How do you use the any type and what are the risks associated with it?

The any type in TypeScript is a type that can represent any value, effectively opting out of type checking. It is useful when you are migrating JavaScript code to TypeScript or when you are dealing with dynamic content where the type is not known at compile time.

Example:

let dynamicValue: any = "Hello, World!";
dynamicValue = 42; // No type error
dynamicValue = true; // No type error

While the any type provides flexibility, it comes with risks. Using any undermines the benefits of TypeScript’s static type checking, making your code more prone to runtime errors. It can lead to issues that are hard to debug and maintain, as type-related errors will not be caught at compile time.

25. How do you use the unknown type and how does it differ from any?

The unknown type in TypeScript is a type-safe counterpart to the any type. While both unknown and any can hold values of any type, unknown requires the developer to perform type checks before performing operations on the variable. This ensures that the code is safer and less prone to runtime errors.

In contrast, the any type allows for any operation to be performed on the variable without any type checking, which can lead to potential runtime errors and makes the code less predictable.

Example:

let value: unknown;
value = "Hello, world!";
value = 42;

if (typeof value === "string") {
    console.log(value.toUpperCase()); // Safe to use string methods
}

let anyValue: any;
anyValue = "Hello, world!";
anyValue = 42;

console.log(anyValue.toUpperCase()); // No type checking, potential runtime error

In the example above, the unknown type requires a type check before using string methods, ensuring type safety. On the other hand, the any type allows the use of string methods without any checks, which can lead to runtime errors if the value is not a string.

26. What are some best practices for using TypeScript in large-scale applications?

When using TypeScript in large-scale applications, several best practices can help ensure code quality, scalability, and maintainability:

  • Strict Type-Checking: Enable strict type-checking options in the tsconfig.json file. This includes settings like strictNullChecks, noImplicitAny, and strictFunctionTypes. These settings help catch potential errors early in the development process.
  • Modularization: Break down the application into smaller, reusable modules. This makes the codebase easier to manage and understand. Use TypeScript’s module system to import and export components, services, and utilities.
  • Consistent Coding Standards: Adopt a consistent coding style across the team. Use tools like TSLint or ESLint with TypeScript support to enforce coding standards and catch common issues.
  • Type Definitions: Use type definitions for third-party libraries. If a library does not have built-in type definitions, consider using DefinitelyTyped or creating custom type definitions.
  • Documentation: Document the code thoroughly using JSDoc comments. This helps new team members understand the codebase and makes it easier to maintain.
  • Testing: Write unit tests and integration tests to ensure the code works as expected. Use testing frameworks like Jest or Mocha with TypeScript support.
  • Refactoring: Regularly refactor the code to improve its structure and readability. TypeScript’s type system makes it easier to refactor code safely.
  • Version Control: Use version control systems like Git to manage the codebase. Create branches for new features, bug fixes, and experiments to keep the main codebase stable.

27. How do you handle asynchronous operations?

In TypeScript, asynchronous operations can be handled using callbacks, promises, and the async/await syntax.

Callbacks are the traditional way of handling asynchronous operations, but they can lead to callback hell, making the code difficult to read and maintain. Promises provide a more elegant way to handle asynchronous operations by allowing chaining and better error handling. The async/await syntax, introduced in ECMAScript 2017, further simplifies working with promises by allowing you to write asynchronous code that looks synchronous.

Example using async/await:

function fetchData(url: string): Promise<any> {
    return fetch(url).then(response => response.json());
}

async function getData() {
    try {
        const data = await fetchData('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

getData();

In this example, the fetchData function returns a promise that resolves with the fetched data. The getData function uses the await keyword to wait for the promise to resolve, making the code easier to read and maintain. The try...catch block is used to handle any errors that may occur during the asynchronous operation.

28. How do you use type assertions and when should they be avoided?

Type assertions in TypeScript allow you to override the type inference by the compiler and specify a type explicitly. This can be useful in scenarios where you are certain about the type of a variable but TypeScript cannot infer it correctly. Type assertions can be done using the as keyword or the angle-bracket syntax.

Example:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

In this example, someValue is of type any, and we use a type assertion to treat it as a string to access the length property.

However, type assertions should be avoided in the following scenarios:

  • When the type is not certain: Using type assertions when you are not sure about the type can lead to runtime errors.
  • When it can be inferred: If TypeScript can infer the type correctly, there is no need to use type assertions.
  • When it bypasses type safety: Type assertions can bypass the type-checking mechanism, which can lead to potential bugs.

29. How do you handle type compatibility and structural typing?

TypeScript uses structural typing, which means that type compatibility is determined by the structure of the types rather than their explicit declarations. This allows for more flexible and intuitive type checking. In TypeScript, two types are considered compatible if their structures match, regardless of the names of the types.

For example, if you have two objects with the same properties and types, they are considered compatible:

interface Point {
    x: number;
    y: number;
}

interface Coordinate {
    x: number;
    y: number;
}

let point: Point = { x: 10, y: 20 };
let coordinate: Coordinate = point; // This is allowed because the structures match

In this example, the Point and Coordinate interfaces are compatible because they have the same structure. This is a key feature of structural typing in TypeScript.

Type compatibility also extends to functions. A function is considered compatible with another if it has the same parameters and return type:

type SumFunction = (a: number, b: number) => number;

let add: SumFunction = (x, y) => x + y;
let sum: (a: number, b: number) => number = add; // This is allowed because the function signatures match

30. What are the differences between interface and type alias, and when would you use each?

In TypeScript, both interfaces and type aliases can be used to define the shape of an object. However, there are some key differences between them:

  • Declaration Merging: Interfaces can be merged, meaning you can define the same interface multiple times, and TypeScript will combine them. Type aliases do not support this feature.
  • Extensibility: Interfaces are generally more extensible. You can extend an interface using the extends keyword, while type aliases can only be extended using intersection types.
  • Usage Scope: Type aliases can define more than just object shapes. They can also define primitives, unions, and tuples, making them more versatile in certain scenarios.

Example:

// Interface example
interface Person {
    name: string;
    age: number;
}

interface Employee extends Person {
    employeeId: number;
}

// Type alias example
type PersonType = {
    name: string;
    age: number;
};

type EmployeeType = PersonType & {
    employeeId: number;
};

In the example above, both interfaces and type aliases are used to define the shape of a Person and an Employee. The interface uses the extends keyword to inherit properties, while the type alias uses intersection types.

Previous

15 Java Collection Framework Interview Questions and Answers

Back to Interview
Next

15 JavaScript Algorithm Interview Questions and Answers