Top 17 TypeScript Interview Questions for 2024: Master Your Next Tech Interview

Top 17 TypeScript Interview Questions for 2024 Master Your Next Tech Interview

Introduction

Are you gearing up for a TypeScript interview in 2024? You’ve come to the right place! TypeScript has become an essential tool in modern web development, and understanding its intricacies can set you apart in the competitive tech job market. In this comprehensive guide, we’ll dive deep into the top 17 TypeScript interview questions you’re likely to encounter this year.

Whether you’re a seasoned developer brushing up on your skills or a newcomer to TypeScript, this article will help you prepare for your upcoming interview. We’ll cover everything from basic concepts to advanced features, providing clear explanations and practical examples along the way.

So, let’s roll up our sleeves and dive into the world of TypeScript. By the end of this guide, you’ll be well-equipped to tackle any TypeScript interview question that comes your way!

1. What is TypeScript and why is it used?

TypeScript is a statically typed superset of JavaScript developed and maintained by Microsoft. It adds optional static typing, classes, and modules to JavaScript, allowing developers to write more robust and maintainable code.

The main reasons for using TypeScript include:

  1. Static Typing: Catches errors at compile-time rather than runtime.
  2. Enhanced IDE Support: Provides better autocompletion, navigation, and refactoring.
  3. Object-Oriented Features: Supports classes, interfaces, and modules.
  4. Improved Maintainability: Makes it easier to work on large-scale applications.
  5. Compatibility: Compiles to plain JavaScript, making it usable in any JavaScript environment.

Here’s a simple example to illustrate the difference:

// JavaScript
function add(a, b) {
    return a + b;
}

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

In the TypeScript version, we specify that a and b should be numbers, and the function should return a number. This helps catch potential errors early in the development process.

2. What are the basic types in TypeScript?

TypeScript includes several basic types that you’ll use frequently:

  1. Boolean: true or false values
  2. Number: Floating point values
  3. String: Textual data
  4. Array: List of values
  5. Tuple: Array with a fixed number of elements
  6. Enum: A set of named constants
  7. Any: Can be assigned any type
  8. Void: Absence of a return value
  9. Null and Undefined: Represent null and undefined values
  10. Never: Represents a type that never occurs
  11. Object: Represents any non-primitive type

Here’s an example using various types:

let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
enum Color {Red, Green, Blue}
let notSure: any = 4;
function warnUser(): void {
    console.log("This is a warning message");
}
let u: undefined = undefined;
let n: null = null;

Understanding these basic types is crucial for writing type-safe TypeScript code.

3. What is the difference between ‘interface’ and ‘type’ in TypeScript?

Both interface and type are used to define custom types in TypeScript, but they have some key differences:

  1. Syntax:
    • interface uses the interface keyword
    • type uses the type keyword and an equals sign
  2. Extensibility:
    • interface can be extended using the extends keyword
    • type can be extended using intersection types (&)
  3. Merging:
    • Multiple interface declarations with the same name are automatically merged
    • type aliases cannot be merged
  4. Computed Properties:
    • type can use computed properties
    • interface cannot use computed properties

Here’s an example illustrating these differences:

// Interface
interface Animal {
    name: string;
}

interface Bear extends Animal {
    honey: boolean;
}

// Type
type Animal = {
    name: string;
}

type Bear = Animal & { 
    honey: boolean;
}

// Merging interfaces
interface Window {
    title: string;
}

interface Window {
    ts: TypeScriptAPI;
}

// Error: Duplicate identifier 'Window'
// type Window = {
//     title: string;
// }

// type Window = {
//     ts: TypeScriptAPI;
// }

// Computed properties
type Keys = 'firstName' | 'lastName';

type DudeType = {
    [key in Keys]: string;
}

// Error: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
// interface DudeInterface {
//     [key in keys]: string;
// }

In general, use interface when you want to define a new object type and type when you want to create aliases for existing types or union types.

4. What are generics in TypeScript and why are they useful?

Generics in TypeScript allow you to create reusable components that can work with a variety of types rather than a single one. They provide a way to make components more flexible and scalable.

The main benefits of using generics include:

  1. Type Safety: Ensures that the type you’re using is consistent throughout the component.
  2. Reusability: Allows you to write flexible, reusable functions and classes.
  3. Reduced Code Duplication: Eliminates the need to write separate functions for different types.

Here’s an example of a generic function:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

In this example, T is a type variable that gets replaced with an actual type when the function is called. This allows the identity function to work with any type while maintaining type safety.

Here’s a more complex example using generics with interfaces:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

In this case, we’re creating a generic interface that describes our identity function, and then using it to create a variable with a specific type.

Generics are particularly useful when working with collections:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

This function is generic and works with arrays of any type, while still providing type safety and intellisense for array methods.

5. What is the ‘readonly’ modifier in TypeScript?

The readonly modifier in TypeScript is used to make a property immutable. Once a property is marked as readonly, it can only be assigned a value when the object is initialized.

Key points about readonly:

  1. It can be used with interface properties, class properties, and array type annotations.
  2. It prevents accidental modification of the property after initialization.
  3. For object literals, it’s checked during excess property checking.

Here are some examples:

// Interface with readonly property
interface Point {
    readonly x: number;
    readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // Error! Cannot assign to 'x' because it is a read-only property.

// Class with readonly property
class Car {
    readonly model: string;
    constructor(model: string) {
        this.model = model;
    }
}

let myCar = new Car("Tesla");
// myCar.model = "Toyota"; // Error! Cannot assign to 'model' because it is a read-only property.

// Readonly array
let readonlyNumbers: readonly number[] = [1, 2, 3, 4];
// readonlyNumbers.push(5); // Error! Property 'push' does not exist on type 'readonly number[]'.

The readonly modifier is particularly useful when you want to ensure that certain properties of an object or elements of an array cannot be changed after their initial assignment.

6. What are decorators in TypeScript?

Decorators are a feature in TypeScript that allow you to add both annotations and metadata to existing code. They provide a way to modify classes, methods, properties, or parameters at design time.

Key points about decorators:

  1. They are a stage 2 proposal for JavaScript and are available as an experimental feature in TypeScript.
  2. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime.
  3. Multiple decorators can be applied to a declaration.

Here’s an example of a simple decorator:

function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyKey} with`, args);
        return originalMethod.apply(this, args);
    };
    return descriptor;
}

class Calculator {
    @logged
    add(x: number, y: number) {
        return x + y;
    }
}

let calc = new Calculator();
calc.add(1, 2);  // Logs: Calling add with [1, 2]

In this example, the @logged decorator logs the method name and arguments whenever the add method is called.

Decorators can also be used with classes:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

The @sealed decorator seals both the constructor and its prototype, preventing new properties from being added to them.

Decorators are powerful tools for metaprogramming in TypeScript, allowing you to modify or enhance classes and their members in a clean, declarative way.

7. What is the ‘as’ keyword used for in TypeScript?

The as keyword in TypeScript is used for type assertions. It tells the compiler to treat a value as a specific type, regardless of how the compiler infers its type.

Key points about as:

  1. It’s a way to tell TypeScript that you know more about the type of a value than it does.
  2. It doesn’t change the runtime behavior of your code; it’s purely a compile-time construct.
  3. It’s often used when working with DOM elements or when you’re certain about a type but TypeScript can’t infer it correctly.

Here are some examples:

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

// Asserting DOM element types
const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement;

// Asserting in complex scenarios
interface Employee {
    name: string;
    code: string;
}

let employee = {} as Employee;
employee.name = "John"; // OK
employee.code = "123"; // OK
// employee.age = 30; // Error! Property 'age' does not exist on type 'Employee'.

// Double assertion
let x = 'hello' as any as number;

In the last example, we’re using a double assertion to bypass TypeScript’s usual type checking. This should be used very carefully, as it can lead to runtime errors if misused.

The as keyword is particularly useful when working with union types:

function getNetPrice(price: number, discount: number, format: boolean): number | string {
    let netPrice = price * (1 - discount);
    return format ? `$${netPrice.toFixed(2)}` : netPrice;
}

let netPrice = getNetPrice(100, 0.05, true) as string;
console.log(netPrice.charAt(0)); // OK

In this case, we’re asserting that netPrice is a string, allowing us to use string methods on it without TypeScript complaining.

Remember, while type assertions can be powerful, they should be used sparingly. It’s generally better to let TypeScript infer types when possible, as overuse of type assertions can lead to less type-safe code.

8. What is the difference between ‘any’ and ‘unknown’ types in TypeScript?

Both any and unknown are top types in TypeScript, meaning they can hold values of any type. However, they behave differently when it comes to type checking:

  1. any:
    • Bypasses type checking completely.
    • Allows access to any property or method without compilation errors.
    • Use of any is generally discouraged as it defeats the purpose of using TypeScript.
  2. unknown:
    • A type-safe counterpart of any.
    • Requires type checking or assertion before you can perform operations on values of this type.
    • Helps maintain type safety while still allowing for dynamic types.

Here’s an example illustrating the differences:

let anyValue: any;
let unknownValue: unknown;

// any allows access to any property without checks
anyValue.foo.bar; // No error
anyValue.method(); // No error

// unknown requires type checking or assertion
// unknownValue.foo.bar; // Error: Object is of type 'unknown'
// unknownValue.method(); // Error: Object is of type 'unknown'

// To use an unknown value, you must first assert or check its type
if (typeof unknownValue === 'object' && unknownValue !== null) {
    (unknownValue as { foo: { bar: string } }).foo.bar; // OK
}

function isFunction(x: unknown): x is Function {
    return typeof x === 'function';
}

if (isFunction(unknownValue)) {
    unknownValue(); // OK
}

In general, unknown is preferred over any when you’re dealing with values whose types you don’t know at compile time. It forces you to perform the necessary type checks before using the value, which helps prevent runtime errors.

9. What are union and intersection types in TypeScript?

Union and intersection types are ways to combine multiple types in TypeScript:

  1. Union Types (|):
    • Represent a value that can be one of several types.
    • The | operator is used to create union types.
  2. Intersection Types (&):
    • Combine multiple types into one.
    • The & operator is used to create intersection types.

Here are examples of both:

// Union type
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
    console.log(`ID is: ${id}`);
}

printId(101); // OK
printId("202"); // OK
// printId({ myID: 22342 }); // Error: Argument of type '{ myID: number; }' is not assignable to parameter of type 'StringOrNumber'.

// Intersection type
interface BusinessPartner {
    name: string;
    credit: number;
}

interface Identity {
    id: number;
    name: string;
}

type Employee = BusinessPartner & Identity;

let e: Employee = {
    name: "John",
    credit: 1000,
    id: 100
};

In the union type example, printId can accept either a string or a number. In the intersection type example, Employee combines all properties from both BusinessPartner and Identity.

Union types are particularly useful when a value could be one of several types:

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

In this example, padding can be either a string or a number, and the function behaves differently based on its type.

Intersection types are often used with interfaces to create new types that combine existing ones:

interface Colorful {
    color: string;
}

interface Circle {
    radius: number;
}

type ColorfulCircle = Colorful & Circle;

let cc: ColorfulCircle = {
    color: "red",
    radius: 42
};

Here, ColorfulCircle is a new type that has all properties from both Colorful and Circle.

10. What are optional properties in TypeScript?

Optional properties in TypeScript are properties that may or may not exist on an object. They are denoted by adding a question mark (?) after the property name.

Key points about optional properties:

  1. They allow you to define objects where some properties might not always be present.
  2. They’re particularly useful when working with APIs or user input where certain fields might be missing.
  3. When accessing optional properties, TypeScript enforces null checks to prevent runtime errors.

Here’s an example:

interface User {
    id: number;
    name: string;
    email?: string;  // Optional property
}

function createUser(id: number, name: string, email?: string): User {
    return { id, name, email };
}

let user1 = createUser(1, "Alice");  // OK
let user2 = createUser(2, "Bob", "bob@example.com");  // Also OK

console.log(user1.email?.toLowerCase());  // Safe access using optional chaining

In this example, email is an optional property. We can create a User with or without an email address. When accessing the email property, we use the optional chaining operator (?.) to safely access it without causing a runtime error if it’s undefined.

Optional properties are also commonly used in function parameters:

function printName(obj: { first: string; last?: string }) {
    // ...
}

printName({ first: "Bob" });  // OK
printName({ first: "Alice", last: "Alisson" });  // OK

This allows the function to be called with or without the last name parameter.

11. What is the ‘keyof’ operator in TypeScript?

The keyof operator in TypeScript is used to get the union of keys from an object type. It produces a string or numeric literal union of the keys of the given type.

Key points about keyof:

  1. It extracts the key type from an object type.
  2. The resulting type is a union of string or numeric literals.
  3. It’s often used with generics to create flexible, reusable code.

Here’s a basic example:

interface Person {
    name: string;
    age: number;
    location: string;
}

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

let key: PersonKeys = "name"; // OK
// let key2: PersonKeys = "salary"; // Error: Type '"salary"' is not assignable to type 'keyof Person'.

In this example, PersonKeys is a union type of all keys in the Person interface.

The keyof operator is particularly useful when combined with generics:

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

let person: Person = {
    name: "John",
    age: 30,
    location: "New York"
};

console.log(getProperty(person, "name")); // OK, returns "John"
// console.log(getProperty(person, "salary")); // Error: Argument of type '"salary"' is not assignable to parameter of type 'keyof Person'.

In this example, getProperty is a generic function that can access properties of any object in a type-safe way. The K extends keyof T constraint ensures that the key parameter is actually a key of the object type T.

12. What are type guards in TypeScript?

Type guards in TypeScript are expressions that perform a runtime check to ensure a value is of a specific type. They allow you to narrow down the type of a variable within a conditional block.

Key points about type guards:

  1. They help TypeScript infer more specific types in certain code blocks.
  2. Common type guards include typeof, instanceof, and custom user-defined type predicates.
  3. They’re particularly useful when working with union types.

Here are some examples of type guards:

// Using typeof
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

// Using instanceof
class Bird {
    fly() {
        console.log("flying...");
    }
}

class Fish {
    swim() {
        console.log("swimming...");
    }
}

function move(pet: Bird | Fish) {
    if (pet instanceof Bird) {
        pet.fly();
    } else {
        pet.swim();
    }
}

// User-defined type guard
interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
    if (isFish(pet)) {
        pet.swim();
    } else {
        pet.fly();
    }
}

In these examples, typeof, instanceof, and the user-defined isFish function are all type guards. They help TypeScript understand what type a variable is within a certain code block, allowing for more precise type checking and better IDE support.

13. What is the ‘never’ type in TypeScript?

The never type in TypeScript represents the type of values that never occur. It’s often used to represent the return type of a function that always throws an exception or one that never returns.

Key points about never:

  1. It’s a bottom type in TypeScript’s type system.
  2. Variables of type never can never have a value.
  3. It’s often used to represent impossible cases in conditional types.

Here are some examples of where never is used:

// Function that always throws an error
function throwError(message: string): never {
    throw new Error(message);
}

// Function with an infinite loop
function infiniteLoop(): never {
    while (true) {}
}

// Exhaustive checking in switch statements
type Shape = Circle | Square;

function getArea(shape: Shape) {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.sideLength ** 2;
        default:
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}

In the last example, if we were to add a new shape to the Shape type without updating the getArea function, TypeScript would give us a compile-time error because the shape variable would no longer be of type never in the default case.

The never type is also useful in conditional types:

type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<string | number | undefined>;  // string | number

In this example, NonNullable removes null and undefined from a type by leveraging conditional types and never.

14. What are literal types in TypeScript?

Literal types in TypeScript allow you to specify exact values that a string, number, or boolean must have. They’re denoted by using specific literal values as types.

Key points about literal types:

  1. They can be string literals, number literals, or boolean literals.
  2. They’re often used in union types to specify a finite set of allowed values.
  3. They can be used to create type aliases with more specific meanings.

Here are some examples:

// String literal types
type Easing = "ease-in" | "ease-out" | "ease-in-out";

function animate(dx: number, dy: number, easing: Easing) {
    // ...
}

animate(0, 0, "ease-in");
// animate(0, 0, "linear"); // Error: Argument of type '"linear"' is not assignable to parameter of type 'Easing'.

// Number literal types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceRoll {
    return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
}

// Boolean literal types
type Bool = true | false;

// Combining literal types with non-literal types
type Padding = number | "small" | "medium" | "large";

function setPadding(padding: Padding) {
    // ...
}

setPadding(10);      // OK
setPadding("small"); // OK
// setPadding("tiny"); // Error: Argument of type '"tiny"' is not assignable to parameter of type 'Padding'.

Literal types are particularly useful when you want to model specific, known values. They can make your code more expressive and catch more errors at compile-time.

15. What is the ‘infer’ keyword used for in TypeScript?

The infer keyword in TypeScript is used within conditional types to infer (extract) a type from another type. It’s particularly useful when working with complex types or when you want to extract types from functions or classes.

Key points about infer:

  1. It can only be used in the extends clause of conditional types.
  2. It declares a type variable that can be referenced in the true branch of a conditional type.
  3. It’s often used to extract return types, parameter types, or generic type arguments.

Here are some examples:

// Extracting return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

function foo() {
    return { a: 1, b: 2 };
}

type FooReturn = ReturnType<typeof foo>; // { a: number, b: number }

// Extracting parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

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

type BarParams = Parameters<typeof bar>; // [number, string]

// Extracting type arguments from generic types
type Unpack<T> = T extends Array<infer U> ? U : T;

type T1 = Unpack<string[]>;  // string
type T2 = Unpack<number>;    // number

In these examples, infer is used to extract types from functions and arrays. The ReturnType utility type infers the return type of a function, Parameters infers the parameter types, and Unpack infers the type of array elements.

The infer keyword is a powerful tool for creating advanced type manipulations and can be particularly useful when working with generic types or when you need to extract types from complex structures.

16. What are mapped types in TypeScript?

Mapped types in TypeScript allow you to create new types based on old types by transforming properties. They’re a way to create new types by iterating over the keys of an existing type.

Key points about mapped types:

  1. They use the syntax { [P in K]: T } where K is often a union of property names or keyof some type.
  2. They can be used to make all properties optional, readonly, or to change their types.
  3. They’re often used with keyof and other type operators to create utility types.

Here are some examples:

// Making all properties optional
type Partial<T> = {
    [P in keyof T]?: T[P];
};

interface Todo {
    title: string;
    description: string;
}

type PartialTodo = Partial<Todo>;
// Equivalent to: { title?: string; description?: string; }

// Making all properties readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

type ReadonlyTodo = Readonly<Todo>;
// Equivalent to: { readonly title: string; readonly description: string; }

// Changing property types
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

type TodoCategories = "work" | "personal" | "shopping";
type TodosByCategory = Record<TodoCategories, Todo[]>;
// Equivalent to: { work: Todo[]; personal: Todo[]; shopping: Todo[]; }

// Removing properties
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type TodoPreview = Omit<Todo, "description">;
// Equivalent to: { title: string; }

In these examples, we’re using mapped types to create new types based on existing ones. The Partial type makes all properties optional, Readonly makes all properties readonly, Record creates an object type with specific keys and value types, and Omit removes specified properties from a type.

Mapped types are a powerful feature in TypeScript that allow for flexible type transformations and are often used in utility types and libraries.

17. What is the difference between ‘extends’ and ‘implements’ in TypeScript?

In TypeScript, extends and implements are both used in class declarations, but they serve different purposes:

  1. extends:
    • Used for class inheritance.
    • A class can extend only one other class.
    • The derived class inherits properties and methods from the base class.
  2. implements:
    • Used to implement an interface.
    • A class can implement multiple interfaces.
    • The class must define all the properties and methods declared in the interface.

Here’s an example illustrating both:

// Base class
class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

// Interface
interface Flyable {
    fly(height: number): void;
}

// Class using extends
class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

// Class using both extends and implements
class Bird extends Animal implements Flyable {
    fly(height: number) {
        console.log(`${this.name} flew to a height of ${height} meters.`);
    }
}

let dog = new Dog('Buddy');
dog.bark();  // Outputs: Woof! Woof!
dog.move(10);  // Outputs: Buddy moved 10m.

let bird = new Bird('Tweety');
bird.fly(100);  // Outputs: Tweety flew to a height of 100 meters.
bird.move(5);  // Outputs: Tweety moved 5m.

In this example:

  • Dog extends Animal, inheriting the name property and move method.
  • Bird extends Animal and implements Flyable, inheriting from Animal and being required to implement the fly method from Flyable.

Key differences:

  • extends creates an “is-a” relationship (a Dog is an Animal).
  • implements creates a “has-a” relationship (a Bird has the ability to fly).
  • A class can extend only one class but can implement multiple interfaces.

Leave a Reply

Your email address will not be published. Required fields are marked *