Hiring TypeScript developers can be challenging, as it requires a keen eye to identify candidates who possess both JavaScript expertise and a solid grasp of TypeScript's type system. Understanding the skills required for a TypeScript developer is crucial for recruiters.
This blog post is structured to provide a comprehensive list of TypeScript interview questions categorized by difficulty: basic, intermediate, advanced, and expert, plus a set of multiple-choice questions. These questions are crafted to evaluate candidates' understanding and application of TypeScript concepts.
By using these questions, you can ensure a well-rounded assessment of candidates. Consider using a TypeScript online test to filter candidates before the interview.
Table of contents
Basic TypeScript interview questions
1. What is TypeScript, simply put?
TypeScript is a superset of JavaScript that adds optional static typing.
In simpler terms, it's JavaScript with extra features that help you catch errors earlier in development. TypeScript code gets compiled down to regular JavaScript, which can then be run in any browser or JavaScript environment. TypeScript provides advantages like:
- Improved code readability: Explicit types make code easier to understand.
- Enhanced tooling: Better autocompletion, refactoring, and navigation in IDEs.
- Early error detection: Type checking helps identify potential bugs before runtime.
- Better code organization: Facilitates building large and complex JavaScript applications.
2. Why might someone choose TypeScript over JavaScript?
TypeScript offers several advantages over JavaScript, primarily centered around static typing. This allows for early detection of errors during development, reducing runtime bugs. TypeScript code is often more maintainable, especially in large projects, because the types provide clear documentation and facilitate refactoring.
Key benefits include:
- Improved code quality: Static typing catches errors before runtime.
- Enhanced maintainability: Type annotations make code easier to understand and modify.
- Better tooling: IDEs offer better autocompletion, refactoring, and error checking.
- Gradual adoption: TypeScript is a superset of JavaScript, allowing for gradual migration. TypeScript compiles to clean, readable JavaScript that can run anywhere JavaScript runs.
3. Can you describe a basic TypeScript type?
A basic TypeScript type defines the kind of values a variable can hold. For example, string, number, and boolean are fundamental types. string represents textual data, number represents numeric values (integers and floating-point numbers), and boolean represents true/false values. We can explicitly annotate a variable with one of these types:
let message: string = "Hello, world!";
let count: number = 42;
let isTrue: boolean = true;
TypeScript also provides other basic types like any (allows any value), void (represents the absence of a value), null and undefined.
4. How do you declare a variable with a specific type in TypeScript?
In TypeScript, you declare a variable with a specific type using a colon (:) followed by the desired type annotation after the variable name. For example, let myVariable: string; declares a variable named myVariable of type string. You can also initialize the variable at the same time like this: let myNumber: number = 10;.
TypeScript supports various built-in types like string, number, boolean, any, void, null, undefined, and more complex types like arrays (string[]), tuples ([string, number]), and custom types using interfaces or classes. For example, a variable that is an array of numbers can be declared as let numbers: number[] = [1, 2, 3];
5. What's the deal with 'any' in TypeScript? When would you use it?
any in TypeScript essentially disables type checking. It allows you to assign any value to an any variable and perform any operation on it without the TypeScript compiler complaining. It's like opting out of TypeScript's type system for a specific variable or expression.
Use any sparingly. It's helpful when you're working with:
- Legacy codebases without type information.
- Third-party libraries that lack type definitions.
- When you're in the early stages of prototyping and the exact type isn't crucial yet.
- Gradually migrating JavaScript to TypeScript.
However, overuse defeats the purpose of TypeScript. Prefer more specific types or unknown (which forces you to perform type checks before using a value) whenever possible to maintain type safety.
6. Explain what a TypeScript interface is.
A TypeScript interface is a way to define a contract for the structure of an object. It specifies the names, types, and optionality of properties that an object must have. Interfaces do not define any implementation; they simply describe the shape of an object.
Essentially, they provide type-checking during development. By using interfaces, you can ensure that objects conform to a specific structure, catching errors at compile time rather than runtime. Code example:
interface Person {
name: string;
age: number;
greet: (message: string) => void; //method signature
}
const john: Person = {
name: "John Doe",
age: 30,
greet(message: string) {
console.log(message + " " + this.name);
}
};
7. How are interfaces useful in TypeScript development?
Interfaces in TypeScript are powerful tools for defining contracts within your code. They specify the shape of an object, outlining the properties it should have and their respective types. This ensures that different parts of your application interact with each other in a predictable and consistent manner.
Interfaces promote code reusability and maintainability. By defining common interfaces, you can ensure that different classes or objects adhere to the same structure. This reduces errors, makes refactoring easier, and improves overall code clarity. They are used for type-checking during compilation, catching errors early. You can also use interfaces for:
- Defining the structure of objects
- Specifying function types
- Describing class types
- Implementing
duck typing
8. What are TypeScript enums and why are they helpful?
TypeScript enums (enumerations) are a way to define a set of named constants. They provide a more readable and maintainable way to work with a fixed set of values, such as status codes, roles, or categories. Instead of using raw numbers or strings, you can use meaningful names.
Enums are helpful because they improve code clarity, reduce errors (by enforcing a limited set of valid values), and make refactoring easier. When a value needs to be changed, you only need to modify the enum definition, rather than searching and replacing the value throughout the codebase. Here's a simple example:
enum OrderStatus {
Pending,
Shipped,
Delivered,
Cancelled,
}
let orderStatus: OrderStatus = OrderStatus.Shipped;
Without enums, you might use numbers (0, 1, 2, 3) or strings ('Pending', 'Shipped', etc.), which are less descriptive and more prone to errors.
9. What's the difference between `null` and `undefined` in TypeScript?
null and undefined both represent the absence of a value, but they differ in their origin and intended use.
undefined typically means a variable has been declared but has not yet been assigned a value. It's also the default value returned when a function doesn't explicitly return anything. TypeScript also uses undefined to indicate a missing optional parameter. In contrast, null is an assignment value. You explicitly assign null to a variable to indicate that it intentionally has no value. It represents the intentional absence of an object value. While both compare loosely ( null == undefined is true), they are distinct types (null === undefined is false).
10. Tell me about TypeScript's `void` type.
In TypeScript, void is a type that represents the absence of a value. It's commonly used as the return type of functions that don't return anything explicitly. Unlike any, void signifies that a function is intentionally not returning a value.
Specifically, a function declared with a void return type should not return any value (using return without an expression or simply reaching the end of the function). While TypeScript allows returning null or undefined when strictNullChecks is off, it's best practice to avoid returning anything when void is specified. It's also important to note that a variable declared as void can only be assigned null, undefined (when strictNullChecks is off) or void returning function.
11. What is a TypeScript class, and how does it relate to JavaScript classes?
A TypeScript class is a blueprint for creating objects with properties (data) and methods (functions). It provides a way to define the structure and behavior of objects in a type-safe manner.
TypeScript classes are essentially a superset of JavaScript classes. Under the hood, TypeScript classes are compiled down to standard JavaScript ES6 classes. TypeScript adds features like static typing, access modifiers (like public, private, and protected), abstract classes, and interfaces that enhance the capabilities of JavaScript classes. These TypeScript-specific features are stripped away during compilation, leaving behind standard JavaScript class syntax that can be executed by any JavaScript runtime.
12. How do you compile a TypeScript file into JavaScript?
To compile a TypeScript file (.ts) into JavaScript (.js), you use the TypeScript compiler, tsc. You can execute the command tsc yourFileName.ts in your terminal. This will generate a JavaScript file with the same name (yourFileName.js) in the same directory.
Alternatively, you can compile multiple TypeScript files or an entire project using a tsconfig.json file. In this case, you would simply run tsc in the directory containing the tsconfig.json file. The tsconfig.json file specifies the compiler options and input files for your project.
13. What are generics in TypeScript, in simple terms?
Generics in TypeScript allow you to write code that can work with a variety of types without knowing those types in advance. Think of them as placeholders for types. They provide a way to make components reusable while maintaining type safety.
For example, instead of writing separate functions for operating on arrays of numbers and arrays of strings, you can write a single function that uses a generic type parameter (often denoted as T) to represent the type of the array elements. This means you can use a single function and specify at the point of function call if you are operating on an array of numbers or strings or any other object that needs to be processed by the same function. T is replaced by the actual type when the function is called. You can see this in the following example:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello"); // type of myString will be string
let myNumber: number = identity<number>(42); // type of myNumber will be number
14. Why would you use generics in TypeScript?
Generics in TypeScript allow you to write code that can work with a variety of types while maintaining type safety. They essentially let you define functions, classes, or interfaces that can operate on different data types without requiring you to write separate implementations for each type. This promotes code reusability and reduces the risk of errors by ensuring type consistency throughout your code.
For example, consider a simple function that returns the first element of an array. Using generics, you could define it like this: function getFirstElement<T>(arr: T[]): T | undefined { return arr[0]; }. Here, T represents a type variable. Now, you can call getFirstElement([1, 2, 3]) (where T becomes number) or getFirstElement(['a', 'b', 'c']) (where T becomes string) and the TypeScript compiler will ensure that the return type is consistent with the type of the array's elements, without having to write separate functions for number arrays and string arrays.
15. What is type inference in TypeScript?
Type inference in TypeScript refers to the compiler's ability to automatically deduce the data type of a variable or expression based on its surrounding context. This means you don't always need to explicitly define the type. TypeScript analyzes the code and infers the type from the assigned value or how the variable is used.
For example, if you initialize a variable with a string, TypeScript will infer that the variable's type is string. Likewise, when a function returns a specific type, TypeScript can often infer that return type without you explicitly defining it. This reduces boilerplate code and makes development faster, while still providing type safety. Here's an example:
let message = "Hello, TypeScript!"; // TypeScript infers 'message' to be of type string
function add(a: number, b: number) { //a and b are explicitly of type number
return a + b; //Typescript infers return type to be number
}
16. How does TypeScript help catch errors during development?
TypeScript enhances error detection during development primarily through static typing. By assigning types to variables, function parameters, and return values, TypeScript's compiler can identify type-related errors before runtime.
Specifically, TypeScript helps catch errors like:
- Type mismatches: Ensures that values assigned to variables are of the expected type.
- Missing properties: Verifies that objects have the required properties.
- Incorrect function arguments: Checks that the correct number and types of arguments are passed to functions.
- Null/undefined errors: Allows for stricter null checking to prevent errors caused by accessing potentially null values using features such as strictNullChecks. For example
let str: string | null = null; console.log(str.length);would error with strictNullChecks enabled. - Invalid code syntax: Enforces stricter syntax compared to JavaScript. TypeScript compilation would fail for syntax errors or issues with using modules/types.
17. What's the purpose of a `tsconfig.json` file?
The tsconfig.json file specifies the root files and the compiler options required to compile a TypeScript project. It essentially acts as a configuration file that guides the TypeScript compiler (tsc) on how to transpile TypeScript code to JavaScript. Without it, the compiler would use default settings, potentially leading to unexpected or incorrect output.
Key functionalities include:
- Specifying TypeScript compiler options (e.g., target ECMAScript version, module system).
- Defining the files to include or exclude in the compilation process.
- Enabling features like strict type checking.
// Example of tsconfig.json{ "compilerOptions": { "target": "es5", "module": "commonjs", "strict": true }, "include": ["src/**/*"] }
18. Can you explain what a union type is in TypeScript?
A union type in TypeScript allows a variable to hold values of different types. It's defined using the pipe (|) symbol to separate the possible types. For example, string | number means a variable can be either a string or a number.
Think of it as "OR". The type system will only allow operations that are valid for all possible types in the union. If you need to perform operations specific to a certain type within the union, you'll often use type narrowing techniques like typeof checks or instanceof.
19. What are literal types in TypeScript?
Literal types in TypeScript allow you to specify the exact value a variable or constant should hold. Instead of just specifying a general type like string or number, you specify a specific string or number literal. For example, instead of let direction: string = 'north', you can use let direction: 'north' = 'north'.
Literal types can be strings, numbers, booleans, or even enum members. They're useful for creating more precise type definitions, improving type safety, and enabling better autocompletion in your code editor. They are often used in union types to define a limited set of acceptable values for a variable. Example: type CardinalDirection = 'north' | 'south' | 'east' | 'west';
20. How do you define a function's parameter types and return type in TypeScript?
In TypeScript, you define a function's parameter types and return type using type annotations. You specify the type after the parameter name, separated by a colon. The return type is annotated after the parameter list, also separated by a colon. Here's an example:
function greet(name: string): string {
return `Hello, ${name}!`;
}
In this example, name is a parameter of type string, and the function greet has a return type of string. If a function doesn't return anything, you can use void as the return type. For example:
function logMessage(message: string): void {
console.log(message);
}
21. What is the difference between type assertion and type casting in Typescript?
In TypeScript, both type assertion and type casting change the way the compiler treats a variable's type, but they do so with different intentions and syntax. Type assertion tells the compiler "trust me, I know what I'm doing" and doesn't perform any runtime checks. It uses the as keyword or the angle bracket syntax (<Type>value). Example: let strLength: number = (someValue as string).length;
Type casting, on the other hand, involves converting a variable from one type to another. JavaScript doesn't have explicit type casting in the traditional sense (like C++), but TypeScript allows you to narrow a type through conditional checks or other means. The type remains the same at runtime, but the compiler treats it differently based on your code's logic. Type assertions are generally used when you're more certain about the type than the compiler is, while type casting (more accurately type narrowing) is based on runtime conditions.
22. Explain what is meant by 'duck typing' in the context of TypeScript?
Duck typing in TypeScript (and other languages) means that the type of an object is determined by its properties and methods, rather than its explicit class or interface declaration. It's based on the principle: "If it walks like a duck and quacks like a duck, then it must be a duck."
In practice, TypeScript's structural typing system allows you to use an object where a specific type is expected, as long as that object has all the required properties and methods of the expected type. This provides flexibility and avoids strict class-based type checking, letting you focus on behavior rather than rigid type hierarchies. This contrasts with nominal typing, where type compatibility depends on explicit declarations and names.
23. What are mapped types in TypeScript and how are they useful?
Mapped types in TypeScript allow you to create new types by transforming properties of existing types. They iterate over the keys of a type and apply a transformation to each property, creating a new type with modified properties. This is achieved using the in operator to iterate over a union of keys. [P in keyof T]: ... is a common pattern where T is the original type and P represents each key.
Mapped types are useful for:
- Readonly properties: Making all properties of a type readonly, e.g.,
Readonly<T>. - Optional properties: Making all properties optional, e.g.,
Partial<T>. - Required properties: Making all properties required, e.g.,
Required<T>. - Picking and Omiting properties: Creating a new type with only certain properties (Pick) or excluding certain properties (Omit).
- Custom transformations: Modifying property types based on their original types.
For example:
type ReadonlyPerson = Readonly<{ name: string; age: number; }>;
24. Describe how you would use TypeScript with React?
Using TypeScript with React involves several key aspects. First, you define the types for your React component's props and state using TypeScript's type system (interface or type). This provides type safety and helps catch errors during development. For example:
interface MyComponentProps {
name: string;
age: number;
}
const MyComponent: React.FC<MyComponentProps> = ({ name, age }) => {
return <div>{name} is {age} years old</div>;
};
Next, you utilize TypeScript's features like generics and type inference to create reusable and type-safe components and functions. You can also use React.useState and React.useEffect hooks with TypeScript, ensuring the correct types are used for state variables and effect dependencies. By enforcing types, TypeScript helps you maintain a more robust and predictable React codebase.
25. What are decorators in TypeScript and what problem do they solve?
Decorators in TypeScript are a special kind of declaration that can be attached to classes, methods, accessors, properties, or parameters. They use the @expression form, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration. Essentially, decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. They solve the problem of adding metadata or modifying the behavior of a class or its members in a clean, reusable, and declarative way.
For example, instead of manually writing code to log method calls or validate input, you can define a decorator that encapsulates this logic. This promotes code reuse and improves readability by separating concerns. Decorators enable features like:
- Logging: Automatically log method calls.
- Validation: Validate method arguments or property values.
- Dependency Injection: Register classes for dependency injection.
- Metadata: Add metadata to classes or members for use by other tools or frameworks.
Here is a simple example:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} called with ${args}, returned ${result}`);
return result;
};
}
class MyClass {
@log
myMethod(arg: number) {
return arg * 2;
}
}
const instance = new MyClass();
instance.myMethod(5); // Output: Method myMethod called with 5, returned 10
26. How does the 'never' type function in TypeScript?
The never type in TypeScript represents values that never occur. It's used for functions that never return (e.g., infinite loops or functions that always throw an error) or for type guards that narrow a type to nothing.
never is a bottom type, meaning it's assignable to every other type. However, no type is assignable to never (except never itself). This makes it useful for ensuring exhaustiveness in conditional type scenarios or discriminated unions, where TypeScript can verify that all possible cases have been handled. Code after a function returning never is considered unreachable, and the compiler will flag it as such.
27. What are conditional types in Typescript? Explain with an example.
Conditional types in TypeScript allow you to define types that depend on a condition. They look like ternary operators in JavaScript, and the type will be resolved based on whether the condition is true or false.
Example:
type Animal = {
name: string;
};
type Dog = {
name: string;
breed: string;
};
type Example<T> = T extends Animal ? Dog : string;
type Result1 = Example<Animal>; // Dog
type Result2 = Example<number>; // string
In this example, Example<T> checks if T is assignable to Animal. If it is, the type resolves to Dog; otherwise, it resolves to string.
28. How do you prevent a variable from being reassigned in TypeScript?
In TypeScript, you can prevent a variable from being reassigned by using the const keyword. When a variable is declared with const, its value cannot be changed after it is initialized.
For example:
const myVariable: string = "Hello";
// myVariable = "World"; // This will cause a compile-time error
Keep in mind that const only prevents reassignment of the variable itself. If the variable holds a reference to an object or an array, the properties of that object or the elements of that array can still be modified.
29. Describe the difference between `readonly` and `const` in Typescript.
readonly and const serve different purposes in TypeScript.
const prevents reassignment of a variable after it's initialized. It's a compile-time construct that affects how the variable is used within the scope where it's declared. For example:
const x = 10;
// x = 20; // Error: Cannot reassign constant variable
readonly, on the other hand, is a TypeScript-specific keyword that applies to properties of classes or interfaces. It prevents modification of the property's value after the object has been created. Unlike const, which prevents reassignment of the variable itself, readonly prevents changing the value of a specific property. For example:
class Example {
readonly y: number;
constructor(y: number) {
this.y = y; // OK within the constructor
}
// modify() {
// this.y = 30; // Error: Cannot assign to 'y' because it is a read-only property.
// }
}
const ex = new Example(20);
// ex.y = 40; // Error: Cannot assign to 'y' because it is a read-only property.
In short, const is for variables, and readonly is for properties of objects. Also const is a javascript keyword, and readonly is a TypeScript keyword.
30. What is a tuple in TypeScript, and when would you use one?
A tuple in TypeScript is a typed array with a pre-defined length and types for each index. Unlike a regular array where all elements must be of the same type, tuples allow you to define an array where each element can be of a different, specific type. For example, let myTuple: [string, number, boolean] = ['hello', 10, true];
You'd use a tuple when you need to represent a fixed-size collection of values with known types at each position. Common use cases include:
- Representing coordinates:
[number, number]for x and y. - Returning multiple values from a function with different types:
function getValues(): [string, number] { return ['value', 123]; }. - Defining the structure of CSV records:
[string, string, number]for name, city, and age.
Intermediate TypeScript interview questions
1. How do you define and use conditional types in TypeScript? Can you provide a practical example?
Conditional types in TypeScript allow you to define types that depend on a condition, similar to a ternary operator in JavaScript. The syntax is T extends U ? X : Y, where if type T is assignable to type U, the resulting type is X; otherwise, it's Y. This allows for dynamic type determination based on type compatibility.
Here's a practical example:
type IsString<T> = T extends string ? true : false;
type Result1 = IsString<string>; // type Result1 = true
type Result2 = IsString<number>; // type Result2 = false
In this example, IsString<T> checks if T is a string. If it is, the type is true; otherwise, it's false. This can be used to create more complex type logic based on the type of a variable.
2. Explain the difference between mapped types and template literal types. When would you use one over the other?
Mapped types transform existing types by iterating over their properties, applying a transformation to each. They're useful for creating new types based on existing ones, like making all properties of a type readonly or nullable. Template literal types, on the other hand, create new string literal types by concatenating string literals and/or string literal types. They're used to create specific string formats, such as generating a set of route strings based on a union of route names.
You'd use mapped types when you need to structurally modify an existing type (e.g., add or remove properties, change property types). You'd use template literal types when you need to generate string literal types that follow a particular pattern or combine existing string literal types in a specific way. For example:
// Mapped Type
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Template Literal Type
type Route = `/${'users' | 'products'}/${string}`;
3. What are discriminated unions and how do they improve type safety? Show with code.
Discriminated unions, also known as tagged unions or variant types, are a way to define a type that can be one of several different types, where each type has a unique "discriminator" or "tag" property. This allows the compiler to know exactly which type it's dealing with at any given time, improving type safety.
Here's a TypeScript example:
// Define the shape types with a common 'kind' property
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// Define the discriminated union type
type Shape = Circle | Square;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
// Exhaustiveness check: if you add a new shape without handling it here, TypeScript will warn you.
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
const myCircle: Circle = { kind: "circle", radius: 5 };
const mySquare: Square = { kind: "square", sideLength: 10 };
console.log(getArea(myCircle)); // Output: 78.53981633974483
console.log(getArea(mySquare)); // Output: 100
In this example, the kind property acts as the discriminator. The getArea function uses a switch statement to determine the correct calculation based on the kind. If we were to add a new shape to the Shape union without updating the getArea function, the default case with the exhaustiveness check would cause a TypeScript error, ensuring that all possible types are handled correctly. This significantly enhances type safety by preventing unexpected behavior or runtime errors that could arise from incorrect type assumptions.
4. Describe how to use the `infer` keyword in conditional types. What problem does it solve?
The infer keyword in TypeScript's conditional types allows you to extract a type from a type you are checking against. It essentially says, "If this type matches a certain pattern, infer a specific type from that pattern and assign it to a type variable". This inferred type can then be used in the true branch of the conditional type.
It solves the problem of extracting parts of a type without explicitly naming them. For example, extracting the return type of a function or the type of elements in an array. Without infer, you'd need to manually define types or use less precise methods to achieve similar results. infer provides a clean and type-safe way to deconstruct and use parts of existing types. Here's an example:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function myFunction(x: number): string { return x.toString(); }
type MyFuncReturnType = ReturnType<typeof myFunction>; // string
5. How does TypeScript handle variance (covariance, contravariance, invariance) in function parameters? Give examples.
TypeScript handles variance in function parameters with some limitations. Function parameters are contravariant in TypeScript. This means that a function type (a: A) => void is assignable to a function type (a: B) => void if B is a subtype of A. This is safe because the function accepting the more general type B can handle any value of type A. Return types, on the other hand, are covariant.
Here's an example:
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }
// Contravariance in parameters
let animalFunc: (a: Animal) => void = (a) => console.log(a.name);
let dogFunc: (d: Dog) => void = (d) => d.bark();
animalFunc = dogFunc; // Error: Type '(d: Dog) => void' is not assignable to type '(a: Animal) => void'.
dogFunc = animalFunc; // This is valid because Animal is more general than Dog
In the above example, assigning animalFunc to dogFunc is valid, demonstrating contravariance because Animal is the base type and therefore more general than Dog. Assigning dogFunc to animalFunc is an error because Dog is a more specific type than Animal.
TypeScript structurally compares types and allows contravariance in parameters, but there are situations where it might not behave exactly as expected due to the structural nature of the type system and the use of inference. Invariance is generally not directly supported, but can be enforced using techniques like using both a parameter and a return type involving the same type parameter.
6. Explain the concept of 'declaration merging' in TypeScript. Provide a use case where it is beneficial.
Declaration merging in TypeScript refers to the compiler merging two or more separate declarations declared with the same name into a single definition. This merged definition has the features of all original declarations. The declarations being merged must share the same name and scope. This merging can occur for interfaces, namespaces, and classes. For interfaces, non-conflicting members are simply unioned, while conflicting members require careful resolution (e.g., function overloads). For namespaces, members are aggregated, effectively concatenating the namespace blocks.
A typical use case is augmenting existing JavaScript libraries or even TypeScript's own built-in types. For example, you might want to add a custom property or method to the global String interface: interface String { customMethod(): string; }. This allows you to extend the functionality of a commonly used type without modifying its original definition, promoting better modularity and avoiding conflicts when upgrading the library.
7. What are ambient declarations (`.d.ts` files) used for? How do you write one for a JavaScript library?
Ambient declarations (.d.ts files) are used to provide type information to TypeScript about JavaScript code that doesn't have its own type definitions. This allows TypeScript to understand and type-check JavaScript libraries, frameworks, or any other JavaScript code you're using in your TypeScript project without rewriting the JavaScript. Essentially, it tells the TypeScript compiler "Hey, this JavaScript code exists, and here's its type information."
To write one for a JavaScript library, you essentially describe the shape of the library's API using TypeScript's type syntax. Here's a basic example:
declare module 'my-js-library' {
export function myFunction(arg: string): number;
export interface MyInterface {
property: boolean;
}
export const myConstant: string;
}
This .d.ts file declares a module named my-js-library, which exports a function myFunction, an interface MyInterface, and a constant myConstant. When you import my-js-library in your TypeScript code, TypeScript will use this declaration file to provide type checking and IntelliSense.
8. How do you create and use custom type guards? Why are they important?
Custom type guards are functions that narrow down the type of a variable within a specific scope. They are defined using the is keyword in their return type annotation. For example:
function isStringArray(value: any): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
Type guards are important because TypeScript can't always infer the precise type of a variable, especially when dealing with any types, union types, or external data. They provide a way to tell the compiler more specific information about a variable's type at runtime, enabling better type checking and eliminating the need for excessive type assertions. This leads to safer and more maintainable code.
9. Explain how to use generics with interfaces and classes in TypeScript. Provide example of a generic repository.
Generics in TypeScript allow you to write reusable code that can work with different types. You can use generics with interfaces to define a contract that specifies the type of data that can be used with the interface. For example, interface Repository<T> { getById(id: number): T; save(entity: T): void; }. Similarly, you can use generics with classes to create classes that can work with different types. For example, class GenericRepository<T> implements Repository<T> { ... }.
Here's a simple example of a generic repository:
interface Repository<T> {
getById(id: number): T | undefined;
save(entity: T): void;
}
class InMemoryRepository<T extends { id: number }> implements Repository<T> {
private items: T[] = [];
getById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
save(entity: T): void {
this.items.push(entity);
}
}
interface User { id: number; name: string; }
const userRepository = new InMemoryRepository<User>();
userRepository.save({ id: 1, name: "John Doe" });
const user = userRepository.getById(1);
console.log(user); // Output: { id: 1, name: "John Doe" }
10. What is the purpose of the `keyof` operator in TypeScript? How can it be used with mapped types?
The keyof operator in TypeScript is used to create a union type of all the keys of a given object type. Essentially, it extracts the keys from a type and makes them available as a type themselves.
keyof can be powerfully combined with mapped types. Mapped types allow you to transform each property in a type, and keyof provides the set of keys to iterate over. For example, type ReadOnly<T> = { readonly [K in keyof T]: T[K] } creates a new type where all properties of T are read-only. Here, keyof T provides all the keys of T, and K in keyof T iterates over each key, applying the readonly modifier.
11. Describe how to use the `Partial`, `Readonly`, `Required`, and `Pick` utility types. Give use cases.
TypeScript utility types help manipulate existing types. Partial<Type> makes all properties in Type optional. A use case is when updating a database record; you might only want to update certain fields, so Partial<Record> allows you to pass only the fields you want to change. Readonly<Type> makes all properties in Type readonly, preventing modification after assignment. This is helpful for ensuring immutability, for example, in a configuration object: Readonly<Config>. Required<Type> makes all properties in Type required, even if they were originally optional. You might use this when you need to ensure all properties are present before processing an object, like form validation: Required<FormData>. Pick<Type, Keys> selects a set of properties Keys from Type to create a new type. A common use case is creating a DTO (Data Transfer Object) with only the necessary fields for a specific API endpoint:
interface User { id: number; name: string; email?: string; }
type UserDTO = Pick<User, "id" | "name">; // UserDTO will have only 'id' and 'name'
12. How can you prevent TypeScript from implicitly assigning the `any` type? What are the best practices?
To prevent TypeScript from implicitly assigning the any type, you can enable the noImplicitAny compiler option. This flag, usually set in the tsconfig.json file, will raise an error whenever TypeScript cannot infer a type and would otherwise default to any. You can add "noImplicitAny": true to the compilerOptions section of your tsconfig.json file.
Best practices include:
- Explicitly define types: Always strive to provide type annotations for variables, function parameters, and return types, especially when the type isn't immediately obvious.
- Use type inference wisely: While explicit types are good, leverage TypeScript's type inference system when it's clear and reduces boilerplate.
- Enable strict mode: Consider enabling the
strictcompiler option, which includesnoImplicitAnyand other strict type-checking rules for enhanced type safety. This is generally recommended for new projects.
13. Explain how to work with tuples in TypeScript, including labeled tuples and rest elements in tuples.
Tuples in TypeScript are a typed array with a pre-defined length and types for each index. They're useful for representing a fixed-size collection of values with known types. You can declare a tuple like this: let myTuple: [string, number] = ['hello', 42];. TypeScript enforces the order and types specified in the tuple definition. Labeled tuples allow you to provide names to each element, improving readability: let person: [name: string, age: number] = ['Alice', 30];. You can access elements using either the index or the label (if provided), like person[0] or person.name.
Rest elements in tuples enable you to specify a variable number of elements of a certain type at the end of the tuple. For example: let line: [string, number, ...number[]] = ['line1', 10, 20, 30];. Here, the tuple starts with a string and a number, followed by zero or more numbers. The rest element must be the last element in the tuple type declaration.
14. How do you define and use recursive types in TypeScript? Provide an example, like a tree structure.
Recursive types in TypeScript are types that refer to themselves within their own definition. They're essential for representing data structures with nested, self-similar components, like trees or linked lists. To define one, you essentially create an interface or type alias that includes a property whose type is the type being defined.
For example, consider a tree structure:
interface TreeNode<T> {
value: T;
children?: TreeNode<T>[];
}
const myTree: TreeNode<number> = {
value: 10,
children: [
{ value: 5 },
{ value: 15, children: [{value:12}] }
]
};
In this example, TreeNode has a children property that's an array of TreeNode objects. This self-reference makes TreeNode a recursive type. The children property is optional so the leaf nodes can be represented easily.
15. What are declaration files and how are they used in TypeScript projects that include JavaScript libraries without TypeScript definitions?
Declaration files (.d.ts) in TypeScript provide type information about existing JavaScript code. They describe the shape of a JavaScript library or module, including its functions, classes, variables, and interfaces, without implementing the actual logic. This allows TypeScript to understand and validate the usage of JavaScript code, even if that code wasn't originally written in TypeScript.
When using a JavaScript library without existing TypeScript definitions, you can create a declaration file to describe its API. This enables TypeScript to provide type checking, autocompletion, and other benefits for that library within your TypeScript project. You can also often find community-created declaration files for popular JavaScript libraries on DefinitelyTyped. To use these definitions, install the corresponding @types/<library-name> package using npm or yarn.
16. Explain the concept of 'type widening' and 'type narrowing' in TypeScript. Give examples of how they occur.
Type widening and narrowing are mechanisms TypeScript uses to refine the type of a variable based on its usage and context. Type widening occurs when TypeScript infers a broader type than initially specified. For example, when you declare a variable with const, TypeScript infers a specific literal type (e.g., "hello" instead of string). However, when using let or var, the type widens to the more general string.
Type narrowing is the opposite; it refines a type to be more specific based on conditional checks or other operations. For example:
function processValue(value: string | number) {
if (typeof value === 'string') {
// Inside this block, TypeScript knows 'value' is a string
console.log(value.toUpperCase()); // Narrowing to string
} else {
// Here, 'value' is narrowed to a number
console.log(value + 10);
}
}
Other examples of narrowing are using instanceof, discriminated unions, and non-null assertion operators. These techniques allow TypeScript to understand the specific type of a variable at a given point in your code, enabling more precise type checking and preventing errors.
17. Describe how to use the `NonNullable` utility type. In what scenarios is it most useful?
The NonNullable<Type> utility type in TypeScript constructs a type by excluding null and undefined from Type. In essence, it takes a type that might be nullable and creates a new type that is guaranteed to not be null or undefined.
It's most useful when you need to ensure that a variable or a property cannot be null or undefined, especially when dealing with types that are potentially nullable due to union types or optional properties. For example, when interacting with APIs or functions that might return null or undefined under certain conditions, using NonNullable can help to enforce stricter type safety and avoid potential runtime errors. Consider let mightBeNull: string | null = null; let definitelyString: string = NonNullable<typeof mightBeNull>;
18. How can you extend existing interfaces or types in TypeScript to add new properties or methods? Provide examples of interface inheritance.
In TypeScript, you can extend existing interfaces or types using extends. This allows you to create new interfaces or types that inherit the properties and methods of the existing ones, and then add new properties or methods as needed. This promotes code reusability and maintainability.
For interface inheritance, you can define a new interface that extends an existing one. For example:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const myDog: Dog = {
name: "Buddy",
breed: "Golden Retriever",
bark: () => console.log("Woof!")
};
In this case, Dog inherits the name property from Animal and adds its own breed property and bark method.
19. Explain how to use index signatures in TypeScript. What problem do they solve when dealing with dynamic data?
Index signatures in TypeScript allow you to define the type of values in an object when you don't know the specific property names in advance. They're particularly useful when working with dynamic data, where the keys of an object might vary. The syntax is [index: string]: type, indicating that any string key in the object will have a value of the specified type. For example, [key: string]: number means any property accessed using a string will return a number, and the property names are not predetermined. This is how to declare one let myObj: {[key:string]: number}.
Index signatures solve the problem of type safety with dynamic data. Without them, TypeScript would not be able to infer the types of properties accessed using dynamic keys. This would force you to use the any type, sacrificing type safety. With index signatures, TypeScript can enforce type constraints even when you don't know the property names at compile time, ensuring that operations performed on the object's properties are type-safe.
20. How do you define and use tagged union types effectively? Illustrate with an example like handling different API response types.
A tagged union (also known as a discriminated union or variant) is a data structure that holds a value of one of several different types, along with a tag indicating which type is currently being held. This allows you to represent values that can take on different forms, which is useful for handling varied data like API responses. To use tagged unions effectively, you first define the possible types and a tag to distinguish them. Then, when using the tagged union, you check the tag to determine the type of the contained value and process it accordingly. This ensures type safety and allows handling different data structures without resorting to generic objects or type casting.
For example, consider an API that returns either a Success with data or an Error with a message. Here's how you might represent this as a tagged union:
type ApiResponse =
| { status: 'success'; data: { items: string[] } }
| { status: 'error'; message: string };
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
console.log('Data:', response.data.items);
} else if (response.status === 'error') {
console.error('Error:', response.message);
}
}
const successResponse: ApiResponse = { status: 'success', data: { items: ['a', 'b'] } };
const errorResponse: ApiResponse = { status: 'error', message: 'Failed to fetch data' };
handleResponse(successResponse);
handleResponse(errorResponse);
In this example, status is the tag, and the different interfaces are the different types that the union can hold.
21. Explain the use of `unknown` type over `any`. What are the benefits of using `unknown`?
The unknown type in TypeScript is a safer alternative to any. While any effectively disables type checking, unknown forces you to perform type assertions or type narrowing before you can use a value of unknown type. This means you must explicitly verify the type of the unknown variable before operating on it, which reduces the risk of runtime errors.
The key benefits of using unknown include:
- Type Safety:
unknownenforces type checks, preventing accidental misuse of values. - Explicit Type Assertions: You are required to explicitly state what type you expect the value to be, enhancing code clarity.
- Gradual Adoption: It allows you to handle situations where the type is initially unknown but can be refined later. You must check what it is at runtime which is far better than
any.
22. How do you handle function overloading in TypeScript? Provide a practical use case.
TypeScript handles function overloading by allowing you to define multiple function signatures for the same function name. Each signature specifies different parameter types and return types. When the function is called, TypeScript uses the provided arguments to determine which signature to use. The implementation must be compatible with all the provided signatures.
A practical use case is creating a create function that can either take a number (representing an ID) and return an object with that ID, or take a full object and return it.
function create(id: number): { id: number };
function create(obj: { id: number }): { id: number };
function create(arg: number | { id: number }): { id: number } {
if (typeof arg === 'number') {
return { id: arg };
} else {
return arg;
}
}
const obj1 = create(123); // Returns { id: 123 }
const obj2 = create({ id: 456 }); // Returns { id: 456 }
Advanced TypeScript interview questions
1. Explain conditional types in TypeScript and provide a use case where they are particularly helpful. Can you show a practical example?
Conditional types in TypeScript allow you to define a type based on a condition. They use a syntax similar to a ternary operator: T extends U ? X : Y. This means if type T is assignable to type U, the resulting type is X; otherwise, it's Y.
One particularly helpful use case is creating utility types for extracting or excluding properties from an object based on their type. For example:
type NonNullablePropertyKeys<T> = { [K in keyof T]-?: T[K] extends null | undefined ? never : K }[keyof T];
interface Person {
name: string;
age: number | null;
address?: string;
}
type RequiredPersonKeys = NonNullablePropertyKeys<Person>; // "name" | "age"
This example uses conditional types to generate a union of keys from the Person interface that are non-nullable. The resulting RequiredPersonKeys type is "name" | "age", because address is optional (and thus potentially undefined) and age is nullable.
2. What are mapped types in TypeScript? Demonstrate how you can use them to create new types based on existing ones, modifying properties as you go.
Mapped types in TypeScript allow you to create new types by transforming properties of existing types. You iterate over the properties of an existing type and apply transformations (e.g., making properties optional, readonly, or changing their types) to create a new type. This promotes code reuse and type safety.
For example:
type OriginalType = {
name: string;
age: number;
};
// Make all properties optional
type OptionalType<T> = { [K in keyof T]?: T[K] };
type OptionalOriginalType = OptionalType<OriginalType>;
// Make all properties readonly
type ReadonlyType<T> = { readonly [K in keyof T]: T[K] };
type ReadonlyOriginalType = ReadonlyType<OriginalType>;
// Change property type
type StringifiedType<T> = { [K in keyof T]: string };
type StringifiedOriginalType = StringifiedType<OriginalType>;
3. How does TypeScript's `infer` keyword work within conditional types? Give an example of how you'd use it to extract a type from a function's return type.
The infer keyword in TypeScript conditional types allows you to deduce or extract a specific type from a type being checked. It essentially introduces a type variable that can be matched against parts of a type structure. If the conditional type's condition is true (i.e., the type being checked matches the pattern using infer), the inferred type variable is returned; otherwise, the other branch of the conditional type is used.
For example, to extract the return type of a function, you could use:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type MyReturnType = ReturnType<typeof myFunction>; // string
In this example, infer R attempts to match the return type of the function type T. If T is indeed a function type, then R becomes the inferred return type which becomes string in case of myFunction.
4. Describe the use of declaration merging in TypeScript. When would you use it, and what are the potential pitfalls?
Declaration merging in TypeScript allows you to combine multiple declarations with the same name into a single declaration. This applies to interfaces, namespaces, classes, and functions. The compiler merges these declarations to create a unified definition.
It's useful for extending existing libraries or modules without modifying their original source code. For example, you could add properties to a globally defined interface or augment a namespace provided by a third-party library to add new functionality. However, pitfalls include potential naming conflicts if merged declarations define properties or functions with the same name but incompatible types. Also, uncontrolled merging can lead to unexpected type definitions, making code harder to understand and maintain. Carefully consider the impact on type safety when using declaration merging.
5. Explain the concept of discriminated unions in TypeScript. How do they improve type safety when dealing with different object shapes?
Discriminated unions (also known as tagged unions or algebraic data types) in TypeScript are a powerful way to represent a type that can be one of several different object types. Each object type in the union has a common, discriminant property (or tag) with a literal type value. This discriminant allows TypeScript to narrow down the specific type within the union at compile time based on the value of this property. This approach allows for exhaustiveness checking to ensure all union members are handled, leading to safer and more predictable code.
By using discriminated unions, TypeScript can leverage its type system to ensure that you only access properties that are valid for the specific type of object you're currently working with. Without discriminated unions, you might have to use type assertions or other less safe methods to access properties, potentially leading to runtime errors. For example:
type Success = { type: "success", result: string };
type Error = { type: "error", message: string };
type Result = Success | Error;
function handleResult(result: Result) {
if (result.type === "success") {
console.log(result.result); // TypeScript knows 'result' is 'Success' here
} else {
console.log(result.message); // TypeScript knows 'result' is 'Error' here
}
}
6. What are recursive types in TypeScript? Illustrate a scenario where you'd need to define a recursive type, like representing a nested object structure.
Recursive types in TypeScript are types that refer to themselves within their own definition. They are particularly useful for representing data structures that have a nested or hierarchical structure, where a property of an object can be of the same type as the object itself.
Consider a scenario where you need to represent a nested object where each node can have a value and a potentially nested child. This could represent a file system directory structure or a nested configuration object. Here's how you might define such a type:
type NestedObject = {
value: string;
child?: NestedObject;
};
const example: NestedObject = {
value: "root",
child: {
value: "child1",
child: {
value: "grandchild1",
},
},
};
In this example, NestedObject is a recursive type because the child property can be another NestedObject.
7. How do you use type guards in TypeScript, and why are they important for narrowing down types within conditional blocks?
Type guards in TypeScript are functions or expressions that narrow down the type of a variable within a specific scope, usually a conditional block. They help the compiler understand the precise type a variable holds, allowing you to safely perform operations specific to that type.
They are important because TypeScript might initially infer a union type (e.g., string | number) for a variable. Without a type guard, the compiler might flag errors if you try to use a method that only exists on one of the types in the union. Type guards like typeof, instanceof, custom type predicates (functions that return arg is Type), and discriminated unions enable you to confidently work with narrowed types within if statements, preventing runtime errors and improving code safety. For example:
function processValue(val: string | number) {
if (typeof val === "string") {
// TypeScript knows val is a string here
console.log(val.toUpperCase());
} else {
// TypeScript knows val is a number here
console.log(val.toFixed(2));
}
}
8. What is the difference between `Pick`, `Omit`, `Partial` and `Required` utility types in TypeScript? Provide examples of when you might use each.
Pick, Omit, Partial, and Required are utility types in TypeScript that help create new types based on existing ones.
Pick<Type, Keys> selects a set of properties from Type whose keys are in Keys. Useful when you only need a subset of properties from an existing type. Example: Pick<User, 'name' | 'email'>.
Omit<Type, Keys> constructs a type by picking all properties from Type and then removing Keys. Useful when you want to exclude certain properties. Example: Omit<User, 'password' | 'role'>.
Partial<Type> makes all properties in Type optional. Useful when creating update forms or objects where not all fields are required initially. Example: Partial<User>.
Required<Type> makes all properties in Type required. Useful when you have a type with optional properties and you need to ensure all properties are defined. Example: Required<Partial<User>>.
9. Explain how you can create a custom utility type in TypeScript. What are some common use cases for custom utility types?
In TypeScript, you can create custom utility types using mapped types, conditional types, and type inference. Mapped types allow you to transform each property in a type. Conditional types enable you to select a type based on a condition. Type inference (using infer) lets you extract parts of a type.
Common use cases for custom utility types include creating types that make properties optional or required, extracting specific properties from an existing type, creating types with read-only properties, or transforming property types based on certain conditions. For example, you might create a Without<T, K> type that removes specific keys (K) from a type (T), or a PickByType<T, U> type that selects properties from type T whose values are assignable to type U. Also use cases are prevalent in React when creating types for props.
10. Describe the use of namespaces and modules in TypeScript. When would you use one over the other, especially in larger projects?
Namespaces and modules in TypeScript both help organize code, but modules are the preferred approach for larger projects. Namespaces create a global scope, which can lead to naming collisions, especially as the codebase grows. They're best suited for small, legacy projects or for simple organizational tasks.
Modules, on the other hand, use file-based organization and explicit imports/exports. This creates a clear dependency graph and avoids global scope pollution. Modules promote better code reusability and testability. For most modern TypeScript projects, and definitely for large ones, using modules (ES modules or CommonJS modules) is the recommended approach. Modules enable better tooling support (bundlers, tree-shaking) and allow for more maintainable code.
11. How does TypeScript handle generics? Explain the concept of generic constraints and provide an example.
TypeScript generics allow you to write code that can work with a variety of types without sacrificing type safety. Generics use type variables (e.g., T) to represent types that are not yet known when the function or class is defined. When the function or class is used, the type variable is replaced with a specific type. Generic constraints allow you to restrict the types that a generic type variable can accept. This ensures that the generic code can safely perform operations specific to the constrained types.
Example:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // Valid, string has length
logLength([1, 2, 3]); // Valid, array has length
// logLength(123); // Invalid, number does not have length
In this example, T extends Lengthwise constrains T to types that have a length property. This allows the function to safely access arg.length.
12. Explain the concept of 'declaration files' (``.d.ts``) in TypeScript. Why are they important, and how do they work with JavaScript libraries?
Declaration files (.d.ts) in TypeScript provide type information about existing JavaScript code. They describe the shape of a JavaScript library or module without including any actual implementation. Think of them as blueprints that tell TypeScript how to understand and use JavaScript code. They are important because they allow you to use JavaScript libraries in your TypeScript code with full type safety and autocompletion, preventing errors that might occur at runtime due to incorrect usage.
When you import a JavaScript library, TypeScript looks for a corresponding .d.ts file. If found, it uses the information in the declaration file to understand the types of the objects, functions, and variables exposed by the library. If no .d.ts file is found, TypeScript will typically infer the types, but this can be less accurate and less informative. Many popular JavaScript libraries have community-maintained declaration files available on DefinitelyTyped which you can install using npm, for example: npm install --save-dev @types/jquery
13. How do you use decorators in TypeScript? Provide an example of creating and using a class decorator.
Decorators in TypeScript are a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. They use the @expression form, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration. To enable experimental support for decorators, you must enable the experimentalDecorators compiler option either via the command line or in your tsconfig.json.
Here's an example of a class decorator:
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;
}
}
In this example, @sealed is the decorator. The sealed function is applied to the Greeter class. The sealed decorator prevents other code from adding or deleting members of the class at runtime, effectively sealing both the constructor and its prototype.
14. What are the different ways to handle `null` and `undefined` in TypeScript to prevent errors? Explain the use of strict null checking.
TypeScript provides several mechanisms to handle null and undefined to prevent errors. One common approach is using strict null checking (enabled via the strictNullChecks compiler option or the strict option). When enabled, null and undefined are distinct types and not assignable to other types unless explicitly allowed. To handle potentially null or undefined values, you can use:
- Optional properties/parameters:
propertyName?: TypeorparameterName?: Typeindicates that the property or parameter is optional and can beundefined. - Union types:
Type | null | undefinedexplicitly declares that a variable can hold a value ofType,null, orundefined. - Nullish coalescing operator (??): Provides a default value if the variable is
nullorundefined(e.g.,value ?? 'default'). - Optional chaining (?.): Safely access properties of an object that might be
nullorundefined(e.g.,object?.property). - Type assertions: Using
!(non-null assertion operator) tells the compiler you're sure a value isn'tnullorundefined(use with caution). - Type narrowing: Using conditional checks (e.g.,
if (value != null)) to refine the type of a variable within a specific block of code. This lets the compiler understand that within theifblock the variable cannot benullorundefined.
15. Explain what is meant by variance in TypeScript (covariance, contravariance, invariance). How can this impact type compatibility?
Variance in TypeScript (and type systems in general) describes how type constructors (like generics or function types) affect the relationship between types. Specifically, it defines whether Type<A> is a subtype of Type<B> given that A is a subtype of B. There are three main kinds:
- Covariance:
Type<A>is a subtype ofType<B>ifAis a subtype ofB. Most commonly seen with return types. For example,Array<string>is a subtype ofArray<string | number>. If a function acceptsArray<string | number>, you can safely provideArray<string>. - Contravariance:
Type<A>is a subtype ofType<B>ifBis a subtype ofA(notice the reverse). Most commonly seen with parameter types in functions. If you have a function that accepts a(animal: Animal) => voidwhereAnimalis the base class andDogis a derived class you can provide a function(animal: Animal) => voidto the same function. This is because the function(animal: Dog) => voidwill accept the animal without any issues. - Invariance:
Type<A>is neither a subtype nor a supertype ofType<B>unlessAandBare exactly the same type. This is commonly seen in mutable data structures to preserve type safety. If aBox<Animal>can contain any animal, you can't treat aBox<Dog>as aBox<Animal>, because you might put aCatinto theBox, thus violating theBox<Dog>'s type safety.Box<Dog>is neither a subtype nor a supertype ofBox<Animal>. In TypeScript, arrays with read and write access are generally invariant.
Variance impacts type compatibility because it determines whether one type can be safely assigned to another. If types are incompatible due to variance issues (e.g., trying to assign an invariant type where a covariant type is expected), the TypeScript compiler will generate a type error.
16. How does TypeScript's `Readonly` type modifier work, and how does it differ from `const`? Give example use cases.
TypeScript's Readonly type modifier makes properties of an object immutable at compile time. Once a Readonly property is initialized, its value cannot be changed. For example:
interface Person {
readonly name: string;
age: number;
}
let person: Person = { name: "Alice", age: 30 };
// person.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property.
person.age = 31; // OK
const prevents reassignment of a variable, meaning the variable will always point to the same object in memory, however it does not make the object itself immutable. Readonly on the other hand makes the properties of an object immutable. You can use Readonly to ensure that data passed to a component or function is not modified, promoting predictability and preventing unintended side effects. Readonly can also be used to define data structures where certain properties should never change after initialization.
17. Explain the concept of tagged types (branded types) in TypeScript and how they can be used for enhanced type safety.
Tagged types (also known as branded types) in TypeScript are a technique to create distinct types based on existing primitive types. They help improve type safety by preventing accidental misuse of values that share the same underlying type. This is achieved by adding a unique "tag" or "brand" property to the type. This property is often never actually present at runtime but is solely for type checking purposes.
For example:
type USD = number & { readonly __brand: unique symbol };
type EUR = number & { readonly __brand: unique symbol };
function formatUSD(amount: USD): string { return `$${amount.toFixed(2)}`; }
function formatEUR(amount: EUR): string { return `€${amount.toFixed(2)}`; }
function convertToUSD(amount:number):USD { return amount as USD; }
const usdAmount = convertToUSD(100); //need to cast to USD
console.log(formatUSD(usdAmount)); // Output: $100.00
//console.log(formatEUR(usdAmount)); // Error: Argument of type 'USD' is not assignable to parameter of type 'EUR'.
const eurAmount: EUR = 50 as EUR;
console.log(formatEUR(eurAmount));
//formatEUR(100); // Error: Argument of type 'number' is not assignable to parameter of type 'EUR'.
In this example USD and EUR are distinct types, even though their underlying type is number. TypeScript will prevent you from accidentally passing a USD value to a function that expects an EUR value, thus improving type safety.
18. Describe how you would implement a type-safe event emitter in TypeScript using generics and discriminated unions.
To implement a type-safe event emitter in TypeScript, I'd use generics and discriminated unions. First, define an interface for the event map, where keys are event names and values are the corresponding event payload types. Next, create a discriminated union type for the events, where each member represents an event with a 'type' property (the event name) and a 'payload' property. The emit function would accept an event from this union. The type of the payload would be determined based on the event type via the discriminated union. The on function would take an event name (key from the event map) and a callback function whose argument type is the corresponding payload type from the event map. This ensures type safety: you can only emit events defined in the event map, and handlers receive the correct payload type. Here's an example:
interface EventMap {
'user-created': { userId: string; username: string };
'item-added': { itemId: number; itemName: string };
}
type EventUnion = {
[K in keyof EventMap]: { type: K; payload: EventMap[K] };
}[keyof EventMap];
The emit function would then ensure only valid payloads are passed:
function emit<T extends EventUnion>(event: T) {
// implementation
}
emit({type: 'user-created', payload: {userId: '123', username: 'test'}});
19. Explain how you can use TypeScript to create a type-safe wrapper around a JavaScript library that doesn't have its own type definitions.
To create a type-safe wrapper around a JavaScript library without existing type definitions, you can use TypeScript's declaration files (.d.ts). First, create a *.d.ts file (e.g., library.d.ts) that describes the library's API. This file will contain type definitions for the library's functions, classes, and variables. You can define these using declare keyword. For example:
declare module 'library-name' {
export function someFunction(arg: string): number;
export interface SomeInterface {
property: boolean;
}
}
Then, import and use the library in your TypeScript code as usual. TypeScript will use the type definitions in the .d.ts file to provide type checking and autocompletion, effectively creating a type-safe wrapper. You'll need to incrementally add definitions to the .d.ts file as you use more of the library's functionality. DefinitelyTyped is a good place to look to see if someone has already created definitions. If not, you will need to write them yourself.
20. Describe the challenges and solutions when migrating a large JavaScript codebase to TypeScript.
Migrating a large JavaScript codebase to TypeScript presents several challenges. Firstly, gradual adoption is crucial, requiring mixed JavaScript and TypeScript code during the transition. This necessitates configuring the TypeScript compiler (tsconfig.json) to allow JavaScript files with .js extensions alongside .ts files. Managing types gradually, starting with simpler modules and progressing to more complex ones, is key. You may need to use any or unknown types initially to avoid immediate type errors, addressing them later.
Secondly, dealing with existing JavaScript libraries without corresponding TypeScript definitions (.d.ts files) can be tricky. You can either install community-maintained type definitions from DefinitelyTyped (using @types/<library-name>) or create your own custom definitions for the parts of the library you use. Finally, be prepared for potential runtime errors masked by JavaScript's dynamic nature, which TypeScript's static typing will now expose during compilation. Using a linter (like ESLint with TypeScript support) will help enforce code style and catch potential issues early on.
Expert TypeScript interview questions
1. Explain the concept of 'declaration merging' in TypeScript and provide a practical use case where it simplifies development.
Declaration merging in TypeScript is the compiler's ability to merge two or more separate declarations declared with the same name into a single definition. These declarations can be interfaces, types, namespaces, or classes. The compiler uses specific rules to determine how the declarations are combined, with later declarations generally taking precedence.
A practical use case is augmenting existing libraries or modules. For example, if you're using a JavaScript library that doesn't have full TypeScript definitions, you can use declaration merging to add type information to existing declarations:
declare module 'some-library' {
interface SomeObject {
newProperty: string;
}
}
//Now you can use 'newProperty' on instances of 'SomeObject'
This avoids modifying the original library's code while still providing type safety in your TypeScript project.
2. How does TypeScript's conditional types feature enable advanced type-level programming? Give an example.
TypeScript's conditional types enable advanced type-level programming by allowing types to be determined based on conditions, much like ternary operators in JavaScript. This allows you to create types that adapt and change their structure depending on other types. They are declared using the A extends B ? X : Y syntax, where A is checked against B, resulting in type X if true, and Y if false.
For example, you can create a type that extracts the return type of a function:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type MyFunc = () => string;
type MyFuncReturnType = ReturnType<MyFunc>; // string
Here, infer R allows us to capture the return type, making the conditional type truly powerful.
3. Describe the differences between TypeScript's 'namespace' and ES modules. When would you use one over the other?
TypeScript's namespace and ES modules both serve to organize code, but they differ significantly in their implementation and usage. namespace creates a single global object and adds properties to it. It's TypeScript-specific and relies on TypeScript's module resolution. ES modules, on the other hand, are a standardized feature of JavaScript (ES6+) and use file-based modules with explicit import and export statements. Each module has its own scope, preventing global namespace pollution.
Use ES modules over namespace for modern JavaScript development. ES modules offer better modularity, tree shaking, and compatibility with standard JavaScript tooling and browsers. namespace is considered legacy and is generally only used in older TypeScript projects or when needing to support older environments lacking ES module support. If you're starting a new project, prefer ES modules. For example:
// ES Module
export function add(a: number, b: number): number { return a + b; }
import { add } from './math';
// Namespace (legacy)
namespace Math {
export function add(a: number, b: number): number { return a + b; }
}
Math.add(1, 2);
4. What are discriminated unions in TypeScript, and how do they improve type safety when dealing with complex data structures?
Discriminated unions (also known as tagged unions or algebraic data types) in TypeScript are a pattern that combines union types with a common, singleton (literal) type property to enable more precise type checking. This 'discriminant' or 'tag' acts as an indicator, allowing TypeScript to narrow down the possible types within the union at compile time. This is essential for handling different data structures safely.
They improve type safety by enabling exhaustive checking. When you switch over a discriminated union, TypeScript can verify that you've handled all possible cases based on the discriminant property. If you miss a case, the compiler will throw an error, preventing potential runtime bugs. Example:
type Success = { type: 'success', value: string };
type Error = { type: 'error', message: string };
type Result = Success | Error;
function handleResult(result: Result) {
switch (result.type) {
case 'success':
console.log(result.value);
break;
case 'error':
console.error(result.message);
break;
}
}
5. How can you leverage TypeScript's 'readonly' modifier to enforce immutability at compile time?
The readonly modifier in TypeScript ensures that a property can only be assigned a value when the object is initially created. After that, any attempt to reassign the property will result in a compile-time error, enforcing immutability.
Here's how you can leverage it:
- Interface/Type Definition: Mark properties as
readonlyin your interfaces or type definitions.
interface Person {
readonly id: number;
name: string;
}
const person: Person = { id: 123, name: "John Doe" };
// person.id = 456; // Error: Cannot assign to 'id' because it is a read-only property.
- Constructor Initialization:
readonlyproperties are typically initialized in the constructor. Once initialized, they cannot be changed. - Benefits: Using
readonlyhelps prevent accidental modification of critical data, improving code reliability and maintainability.
6. Explain how to create and use custom type guards in TypeScript to narrow down the type of a variable within a specific scope.
Custom type guards in TypeScript are functions that return a type predicate. A type predicate takes the form variable is Type, signaling to the compiler that if the function returns true, the variable is of the specified Type. To create one, define a function that performs a runtime check to determine if a variable adheres to a certain type. For example:
function isStringArray(value: any): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === "string");
}
function process(input: any) {
if (isStringArray(input)) {
// Within this block, 'input' is known to be a string[]
input.forEach(str => console.log(str.toUpperCase()));
} else {
console.log("Input is not a string array.");
}
}
In this example, isStringArray is the type guard. When isStringArray(input) returns true inside the if statement, TypeScript narrows the type of input to string[] within that block, allowing you to safely use array methods like forEach and string methods like toUpperCase.
7. Describe the concept of 'covariance' and 'contravariance' in TypeScript, and how they relate to type compatibility.
In TypeScript (and other languages with subtyping), covariance and contravariance describe how type compatibility behaves for generic types and function types, specifically with respect to their type parameters or parameter/return types. Covariance means that a generic type MyType<Subtype> is assignable to MyType<Supertype> if Subtype is a subtype of Supertype. For example, Array<string> is assignable to Array<string | number> because string is a subtype of string | number. In TypeScript, array types are covariant.
Contravariance, on the other hand, means that MyType<Supertype> is assignable to MyType<Subtype> if Subtype is a subtype of Supertype. This is the opposite of covariance. A key area where this applies in TypeScript is function parameters. A function type (arg: Supertype) => void is assignable to (arg: Subtype) => void. This is because a function that accepts a more general type (Supertype) can safely handle a more specific type (Subtype). Function return types are covariant. So () => Subtype is assignable to () => Supertype because a function that returns Subtype can be used where Supertype is expected.
8. How do you handle errors in asynchronous TypeScript code, ensuring proper type safety and avoiding potential runtime exceptions?
In asynchronous TypeScript, error handling emphasizes type safety. I primarily use try...catch blocks within async functions to catch errors. TypeScript's type system helps ensure that caught errors are handled appropriately based on their expected types. For instance:
async function fetchData(): Promise<DataType> {
try {
const response = await fetch('...');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json() as DataType;
} catch (error: any) { // Type annotation for the error
console.error('Error fetching data:', error);
// Handle error (e.g., retry, display message)
throw error; // Re-throw or return a default value
}
}
Additionally, I employ Promise.catch() for handling errors that occur within Promises. TypeScript infers error types if possible, but explicit type annotations for errors ensure type safety. Finally, I leverage discriminated unions to represent successful and error states, providing explicit type information about results.
9. What is the purpose of the 'unknown' type in TypeScript, and when should you use it instead of 'any'?
The unknown type in TypeScript represents a value that could be anything, similar to any. However, unlike any, unknown forces you to perform type checking or type assertions before you can operate on it. This provides type safety that any lacks. unknown is the type-safe counterpart of any.
You should use unknown instead of any when you want to accept values of any type but still maintain type safety. This is particularly useful when dealing with data from external sources (like APIs) where the type is not guaranteed or known beforehand. By using unknown, you're forced to validate the data before using it, preventing potential runtime errors. If you use any you essentially turn off the TypeScript type checker for the particular usage and potentially introduce bugs.
10. Explain how to use mapped types in TypeScript to transform existing types into new ones based on specific transformations.
Mapped types in TypeScript allow you to create new types by transforming properties of existing types. They iterate over each property in the original type and apply a transformation to produce a corresponding property in the new type. This is often done using the keyof operator to get the union of keys from the original type, and then using the in operator to iterate over those keys.
For example, type Readonly<T> = { readonly [K in keyof T]: T[K] }; creates a new type where all properties of the original type T are readonly. Here, K represents each key in T, and T[K] represents the type of the property at key K. You can also add or remove modifiers like ? for optional properties, or perform more complex transformations based on conditional types.
11. Describe the differences between 'interface' and 'type' aliases in TypeScript, highlighting their respective strengths and weaknesses.
In TypeScript, both interface and type aliases can be used to define the shape of an object. However, key differences exist.
interface declarations are primarily used for defining the structure of objects. They support declaration merging, meaning multiple interfaces with the same name can be automatically merged by the compiler into a single declaration. This can be useful for extending existing interfaces. type aliases, on the other hand, create a new name for a type. They offer more flexibility, allowing you to define union types, tuple types, and other more complex type constructs that interfaces can't directly represent. They don't support declaration merging, which can provide better control over type definitions and prevent accidental modifications. type aliases can also be used for primitive types such as string, number, etc. whereas interface is generally for object shapes.
Strengths and weaknesses:
- Interface
- Strength: Declaration merging, clear for object structures.
- Weakness: Limited to object shapes, no union/tuple types.
- Type
- Strength: Versatile, supports complex types (unions, tuples, etc.), can alias primitive types.
- Weakness: No declaration merging.
12. How can you use TypeScript's 'decorators' feature to add metadata or modify the behavior of classes, methods, or properties?
TypeScript decorators are a way to add metadata or modify the behavior of classes, methods, accessors, properties, or parameters. A decorator is essentially a function that is prefixed with @ and placed before the declaration it is decorating. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.
For example, you can use a decorator to log method calls, validate property values, or register classes with a dependency injection container. Here's a simple example:
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
class MyClass {
@logMethod
myMethod(arg1: number, arg2: string): string {
return `Result: ${arg1 + 10} - ${arg2}`;
}
}
const instance = new MyClass();
instance.myMethod(5, "hello");
13. Explain how to create and use generic constraints in TypeScript to restrict the types that can be used with a generic type parameter.
Generic constraints in TypeScript allow you to limit the types that a generic type parameter can accept. This ensures type safety and allows you to work with specific properties or methods of the type within your generic function or class.
To create a constraint, you use the extends keyword. For example, function logName<T extends { name: string }>(obj: T) { console.log(obj.name); }. Here, T is constrained to be any type that has a name property of type string. You can also constrain to a specific type or union of types, e.g., T extends string | number. You can also constrain based on another generic parameter T extends U.
14. Describe the concept of 'definite assignment assertion' in TypeScript, and when it is appropriate to use it.
The definite assignment assertion in TypeScript allows you to tell the compiler that a property of a class or a variable will definitely be assigned a value at runtime, even though the compiler cannot statically prove it. It uses the ! (exclamation mark) postfix operator. It is used when the TypeScript compiler flags a potential uninitialized property or variable, even though you, as the developer, know it will be initialized before being used.
It's appropriate to use it in scenarios like:
- When dealing with complex initialization logic that the compiler can't follow.
- When a property is initialized by a library or framework outside of the constructor (e.g., dependency injection).
- To avoid strict null checks when you are certain a variable will have a value before it's accessed.
15. How can you use TypeScript's 'module augmentation' feature to add new properties or methods to existing modules or libraries?
TypeScript's module augmentation allows you to add new declarations to existing modules. You can achieve this by declaring a module with the same name as the existing one. Inside this module declaration, you can define the new properties, types, or functions you want to add. TypeScript will then merge these declarations with the original module definition.
For example, to add a greet method to the String prototype, you'd create a .d.ts file (or augment in your .ts file) like this:
declare global {
interface String {
greet(): string;
}
}
String.prototype.greet = function() {
return `Hello, ${this}`;
};
This adds the greet method to all string instances globally. Note that you'll typically need to declare these augmentations in a .d.ts file so they are accessible globally.
16. Explain how to create and use custom JSX typings in TypeScript to ensure type safety when working with React or other JSX-based frameworks.
To create custom JSX typings in TypeScript, you typically declare a module that augments the existing JSX namespace. Inside this module, you can define interfaces for intrinsic elements and component props. For example, to define typings for a custom my-element element, you'd declare an interface like interface IntrinsicElements { 'my-element': { prop1: string; prop2?: number; } }. This tells TypeScript what props the custom element my-element should accept, and validates it during compile time. Remember to include a global declaration file (e.g., global.d.ts) where you define your module augmentation to ensure the typings are available throughout your project.
To use these typings, simply use the custom element in your JSX code. TypeScript will then check if the props you're passing to the element match the defined interface. For React components, defining the prop interface and using React.FC<YourPropsInterface> will achieve type-safe props. For example: interface MyComponentProps { name: string; age: number; } const MyComponent: React.FC<MyComponentProps> = ({ name, age }) => <div>{name} is {age} years old</div>;
17. Describe the purpose of the 'declare' keyword in TypeScript, and how it is used to interact with existing JavaScript code or external libraries.
The declare keyword in TypeScript is used to tell the TypeScript compiler that a variable, function, class, or module exists elsewhere. It essentially promises the compiler that the declared entity is defined in another place, such as in an external library or in existing JavaScript code. The declare keyword doesn't create new entities; it only provides type information for existing ones.
It's primarily used to integrate TypeScript with JavaScript code or external JavaScript libraries that don't have TypeScript type definitions (.d.ts files). For example, if you're using a JavaScript library without corresponding type definitions, you can use declare to define the types of the library's functions and variables, allowing TypeScript to type-check your code that uses that library. Without declare, TypeScript would flag errors because it wouldn't know about the external JavaScript code. Example:
declare function myExternalFunction(arg: string): number;
18. How can you use TypeScript's 'project references' feature to improve build times and code organization in large projects?
TypeScript project references enable you to structure your large projects into smaller, more manageable parts and improve build times through incremental builds. By breaking down your project into separate sub-projects (e.g., modules or libraries), TypeScript can build only the changed parts and their dependencies, instead of rebuilding the entire project every time. This dramatically speeds up the build process.
To leverage project references:
- Create separate
tsconfig.jsonfiles for each sub-project. - In the main project's
tsconfig.json, use thereferencesarray to specify the paths to the sub-projecttsconfig.jsonfiles. This establishes dependencies. - Ensure that each sub-project outputs declaration files (
.d.ts) by settingdeclaration: truein itstsconfig.json. These declaration files are used by dependent projects for type checking without needing to rebuild the entire dependency.
Example:
// main tsconfig.json
{
"compilerOptions": { ... },
"references": [
{ "path": "./moduleA" },
{ "path": "./moduleB" }
]
}
19. Explain the concept of 'liveness' and 'reachability' in garbage collection, and how TypeScript can help prevent memory leaks.
Liveness in garbage collection refers to whether an object is still 'alive' or in use by the program. A live object is one that can be reached by tracing a path from the root objects (e.g., global variables, stack frames) to that object. Reachability is the ability of the garbage collector to find and access an object through these established paths. If an object is not reachable, it is considered garbage and is eligible for collection to free up memory.
TypeScript, with its static typing, can help prevent memory leaks by catching potential errors at compile time that could lead to unintended object retention. For example, if a TypeScript program has a class with a dispose method that is never called, then the dispose method is not de-referencing properly, leading to a memory leak. By using strict null checks (strictNullChecks) and explicit type definitions, TypeScript can help identify cases where objects are not properly released or de-referenced, thereby reducing the risk of memory leaks. Additionally, utilizing patterns like the using statement (or its equivalents in other languages) can help ensure that resources are properly released when they are no longer needed.
20. Describe the differences between structural typing and nominal typing, and how TypeScript's structural typing system affects code compatibility.
Structural typing (also known as duck typing) focuses on the shape of a type. Two types are considered compatible if they have the same properties and methods, regardless of their declared names or inheritance. Nominal typing, on the other hand, relies on explicit declarations. Two types are compatible only if they have the same name or if one is explicitly a subtype of the other (e.g., through inheritance or interface implementation).
TypeScript uses structural typing. This means that if two objects have the same members, TypeScript treats them as compatible, even if they are defined with different class or interface names. This can lead to greater flexibility in code, allowing you to pass objects between different parts of your application without needing to explicitly declare that they conform to a specific interface. However, it can also potentially lead to unexpected behavior if you're not careful, as two types with identical structures might represent semantically different concepts. For example:
interface Point { x: number; y: number; }
interface Vector { x: number; y: number; }
let point: Point = { x: 10, y: 20 };
let vector: Vector = point; // Valid in TypeScript because of structural typing
21. How do you use TypeScript to define types for complex data structures, such as trees or graphs, ensuring type safety and preventing runtime errors?
TypeScript excels at defining complex data structures like trees and graphs using interfaces, type aliases, and generics. For example, a tree node can be defined with an interface that specifies properties like value and children, where children is an array of the same node type. Generics allow you to create reusable types that can handle different data types within the tree or graph (e.g., TreeNode<T>).
Type safety is ensured through these explicit type definitions. When manipulating the data structure, TypeScript's compiler checks that you're using the correct types, preventing common runtime errors such as accessing non-existent properties or passing incorrect data. Using readonly can prevent accidental mutation. Discriminated unions are also helpful for representing nodes of different types in the same data structure. Example: interface TreeNode<T> { value: T; children: TreeNode<T>[]; }
22. Explain how you would design and implement a type-safe event emitter using TypeScript's advanced type system features.
To design a type-safe event emitter in TypeScript, I would leverage generics and mapped types. First, define an interface for the event payload types, like interface Events { 'event1': { data: string }, 'event2': { value: number } }. Then, use a generic type EventEmitter<T extends Events> where T represents the event types. The emit function would be defined as emit<K extends keyof T>(event: K, payload: T[K]). The on function would be on<K extends keyof T>(event: K, listener: (payload: T[K]) => void). This ensures that the payload passed to the event listener matches the expected type for that specific event. Using keyof ensures type safety when specifying event names, and the mapped type T[K] enforces correct payload typing.
For implementation, I would maintain a private map of event names to arrays of listeners. The emit function would retrieve the listeners for the given event name and call each listener with the payload. The on function would add the listener to the array for the given event name. TypeScript's type system verifies that the payload passed to emit and received by on matches the declared event type, preventing runtime type errors. Example:
interface Events { data: { message: string } }
const emitter = new EventEmitter<Events>();
emitter.on('data', (payload) => { console.log(payload.message); });
emitter.emit('data', { message: 'Hello' });
23. Describe the challenges of migrating a large JavaScript codebase to TypeScript, and the strategies you would use to minimize disruption and ensure a smooth transition.
Migrating a large JavaScript codebase to TypeScript presents several challenges. Gradually introducing TypeScript and addressing issues incrementally is crucial. Some challenges include: dealing with implicit any types, integrating TypeScript build processes with existing JavaScript build systems, handling third-party JavaScript libraries without TypeScript declarations (d.ts) and the initial overhead of adding types to existing code.
Strategies to minimize disruption include: incremental adoption (converting files one by one), using allowJs: true in tsconfig.json to allow TypeScript to compile alongside JavaScript, leveraging DefinitelyTyped for community-maintained type definitions, using // @ts-nocheck or // @ts-ignore directives sparingly to temporarily suppress errors, and focusing on the most critical or error-prone areas of the codebase first. Also, defining and enforcing strict typing rules gradually helps ensure a smooth transition. For example, you can start with strict: false in tsconfig.json and then enable stricter rules (strictNullChecks, noImplicitAny, etc.) one by one as the codebase becomes more type-safe.
24. How do you use TypeScript to create a domain-specific language (DSL) with custom syntax and semantics, providing a more expressive and type-safe way to solve specific problems?
TypeScript can be used to create a DSL by leveraging its type system and metaprogramming capabilities. We can define custom types and interfaces to represent the domain's concepts. Then, using techniques like tagged template literals, or fluent interfaces, we can create a more expressive syntax. For instance, we can build a configuration DSL where code like configureServer({ port: 8080, routes: [ route("/", handler) ] }) is type-checked and provides auto-completion.
To enforce custom semantics, we rely on functions and classes to interpret this custom syntax. These components handle the DSL's logic and translate it into the underlying system's actions. Additionally, features like decorators can inject behavior or validate rules within the DSL, providing an extra layer of type safety and control. Essentially, we are using TypeScript's features to model domain concepts as types and express domain logic as code.
25. Explain how TypeScript's type system can be used to enforce security policies and prevent common vulnerabilities, such as cross-site scripting (XSS) or SQL injection.
TypeScript's type system can enforce security policies by ensuring data conforms to expected structures and formats, reducing the risk of vulnerabilities. For example, defining specific types for user inputs can prevent XSS by restricting the characters allowed (e.g., escaping special characters using a utility function and defining an EscapedString type). Similarly, for database interactions, you can define types that strictly validate SQL queries, helping to mitigate SQL injection risks by preventing the direct insertion of user-provided strings into SQL statements. Consider a type that only allows certain commands:
type SafeSQLCommand = 'SELECT' | 'UPDATE' | 'DELETE';
function executeQuery(command: SafeSQLCommand, ...args: any[]) {
// ...
}
executeQuery('SELECT', '*'); // Okay
executeQuery('DROP TABLE users;', '*'); // TypeScript Error: Argument of type '"DROP TABLE users;"' is not assignable to parameter of type 'SafeSQLCommand'.
While TypeScript doesn't eliminate these vulnerabilities entirely, it adds a strong layer of compile-time checking that helps developers catch potential security flaws early in the development process. Remember to validate & sanitize at runtime too, this is just an additional layer of protection.
26. Describe how you would use TypeScript to build a type-safe API client for a RESTful web service, automatically generating types from the API schema.
To build a type-safe API client for a RESTful service in TypeScript, I'd leverage tools that generate types from an API schema (like OpenAPI/Swagger). First, I'd use a tool like openapi-typescript or swagger-typescript-codegen to automatically generate TypeScript types from the API's OpenAPI/Swagger specification. These tools parse the schema and produce TypeScript interfaces/types representing the request and response structures for each endpoint.
Next, I'd use a library like axios or fetch to make the actual HTTP requests. I would then create wrapper functions for each API endpoint, using the generated types to ensure type safety. For example:
import {Pet, CreatePetRequest} from './api-types';
import axios from 'axios';
async function createPet(petData: CreatePetRequest): Promise<Pet> {
const response = await axios.post<Pet>('/pets', petData);
return response.data;
}
This approach ensures that the data passed to and received from the API adheres to the defined schema, catching type errors at compile time and greatly improving the reliability of the application.
TypeScript MCQ
Which of the following statements BEST describes the 'never' type in TypeScript?
options:
Which of the following statements best describes how TypeScript handles structural typing (also known as 'duck typing')?
What is the key difference between using an interface and a type alias to define the shape of an object in TypeScript?
options:
Which of the following best describes the primary benefit of using discriminated unions in TypeScript?
Options:
What is the primary purpose of the Readonly<T> utility type in TypeScript?
Options:
Which of the following is the correct way to perform a type assertion in TypeScript?
Options:
What is the primary purpose of the keyof operator in TypeScript?
Options:
Which of the following best describes the use case for index signatures in TypeScript?
options:
What is the primary purpose of the Partial<T> utility type in TypeScript?
What is the primary use case of the Omit utility type in TypeScript?
options:
What is the primary use case of the Record<Keys, Type> utility type in TypeScript?
What is the primary purpose of the Exclude<T, U> utility type in TypeScript?
options:
What is the primary function of the Pick<Type, Keys> utility type in TypeScript?
What is the primary purpose of Mapped Types in TypeScript?
What is the primary use case of the ReturnType<Type> utility type in TypeScript?
options:
What is the primary use case of conditional types in TypeScript?
Options:
What is the primary purpose of the Extract<Type, Union> utility type in TypeScript?
Options:
What is the purpose of the non-null assertion operator (!) in TypeScript?
What is the primary purpose of the Required<T> utility type in TypeScript?
What does the Parameters<Type> utility type in TypeScript do?
Options:
What is the primary purpose of a type predicate function in TypeScript?
Options:
Which of the following is the correct way to define a generic function in TypeScript that only accepts arguments which are objects and have a 'name' property of type string?
Which of the following is the correct way to define a generic function in TypeScript that accepts an array of any type and returns the same array?
What is the primary benefit of using function overloads in TypeScript?
options:
What is the primary purpose of declaration merging in TypeScript?
Options:
Which TypeScript skills should you evaluate during the interview phase?
You can't assess everything about a candidate in a single interview. However, when evaluating TypeScript skills, focusing on core competencies will help you identify strong candidates. Let's explore the key areas to evaluate during your TypeScript interviews.
TypeScript Fundamentals
Screen candidates on their TypeScript knowledge with a skill assessment. Our TypeScript test includes questions on these topics to help you filter candidates.
Test their understanding by asking them to explain the difference between interface and type in TypeScript. This will reveal how well they grasp these fundamental concepts.
Explain the difference between interface and type in TypeScript. When would you use one over the other?
Look for explanations that highlight that interfaces are primarily for defining object structures and can be implemented by classes. Types, on the other hand, can represent unions, intersections, and more complex type manipulations.
Object-Oriented Programming (OOP) Principles
Gauge the candidate's grip on OOP principles. You can assess this with MCQs, and you can find some relevant questions in our C# online test, even though it is not in TypeScript, OOP principles can still be evaluated.
Ask a question that requires applying OOP principles to a practical scenario. This shows how they approach design problems.
Describe how you would use inheritance to create different types of vehicles (e.g., Car, Truck, Motorcycle) in a TypeScript application.
The candidate should explain how they would create a base Vehicle class with common properties and methods, and then derive specific vehicle types from it. This shows they get inheritance.
Generics
You can assess a candidate's knowledge of generics with a well-designed assessment. Check out TypeScript test in our library to help you with this.
Present a scenario where generics can simplify code and improve type safety. Ask the candidate to implement a generic function to solve the problem.
Write a generic function in TypeScript that reverses an array of any type.
A good response should provide a function that correctly reverses the array while preserving type information. Look for understanding of type parameters and their usage within the function.
Hire Skilled TypeScript Developers with Confidence
Looking to hire top TypeScript talent? Accurately assessing their skills is key to building a strong development team.
Skill tests are a great way to gauge a candidate's abilities. Use our TypeScript online test or our Angular TypeScript test for a comprehensive evaluation.
Once you've identified the best candidates through skills tests, you can invite them to interviews for a more in-depth assessment.
Ready to find your next TypeScript expert? Sign up and start assessing candidates today.
TypeScript Online Test
Download TypeScript interview questions template in multiple formats
TypeScript Interview Questions FAQs
Basic TypeScript interview questions often cover data types, interfaces, and basic syntax. They assess a candidate's fundamental understanding of the language.
Intermediate TypeScript interview questions explore topics like generics, advanced types, and working with the DOM. They evaluate a candidate's ability to use TypeScript in more complex scenarios.
Advanced TypeScript interview questions might cover topics like decorators, mixins, and conditional types. These questions gauge a candidate's depth of knowledge and experience with TypeScript.
Expert TypeScript interview questions often involve architectural patterns, performance optimization, and deep understanding of the TypeScript compiler. These questions help identify candidates with a mastery of TypeScript.
Using a range of TypeScript interview questions, from basic to expert, helps you thoroughly assess a candidate's skills and experience. This ensures you hire developers who can effectively contribute to your team's projects.
40 min skill tests.
No trick questions.
Accurate shortlisting.
We make it easy for you to find the best candidates in your pipeline with a 40 min skills test.
Try for freeRelated posts
Free resources