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.
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 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:
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.
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.
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();
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());
In TypeScript, access modifiers control the visibility of class members. The three main access modifiers are:
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
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.
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.
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.
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.
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);
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
When using TypeScript in large-scale applications, several best practices can help ensure code quality, scalability, and maintainability:
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.
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:
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
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:
extends
keyword, while type aliases can only be extended using intersection types.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.