Web Development
TypeScript Questions
A focused collection of the most critical TypeScript interview questions covering type system internals, advanced types, and best practices for modern development.
Core TypeScript Concepts15
TypeScript is a strongly typed superset of JavaScript that compiles to plain JavaScript. It adds static type checking, allowing developers to catch errors during development rather than at runtime. It improves code quality, provides better IDE tooling through autocomplete, and makes large-scale application maintenance significantly easier and safer.
'any' opts out of type checking entirely, making it unsafe. 'unknown' is the type-safe counterpart; you must perform type checking before performing operations on it. 'never' represents values that will never occur, such as a function that always throws an error or an infinite loop.
Interfaces are primarily used to define object shapes and support declaration merging (adding fields to the same name). Type aliases are more flexible, supporting unions, intersections, and primitives. Generally, use interfaces for objects that might be extended and types for complex logic or combined types.
Union types (using '|') allow a variable to hold one of several types (e.g., string or number). Intersection types (using '&') combine multiple types into one, requiring the resulting object to satisfy all combined members, which is common when merging different data structures together.
Type inference is TypeScript's ability to automatically determine the type of a variable based on its assigned value. This reduces the need for explicit type annotations while still maintaining type safety, allowing the compiler to provide errors if you later attempt to assign an incompatible value.
Type assertion tells the compiler to treat a value as a specific type, effectively saying 'I know better than you.' The 'as' syntax is preferred (e.g., value as string), especially in JSX/React, while the angle-bracket syntax (<string>value) can conflict with component tags in modern frameworks.
'undefined' typically means a variable has been declared but has not yet been assigned a value. 'null' is an assignment value that represents a deliberate 'no value.' In TypeScript, these are treated as distinct types when the 'strictNullChecks' flag is enabled in configuration.
Generics allow you to create reusable components that work with a variety of types rather than a single one. They act as 'type variables' that preserve the specific type information across function calls or class instances, ensuring type safety without sacrificing the flexibility of the code.
tsconfig.json is the configuration file for the TypeScript compiler (tsc). Important options include 'target' (JavaScript version), 'module' (import system), 'strict' (enables all strict type-checking options), 'outDir' (where compiled files go), and 'rootDir' to manage the input source file directory structure.
strictNullChecks is a compiler flag that prevents 'null' and 'undefined' from being assigned to other types unless explicitly included in a union. This forces developers to handle potential null values, drastically reducing the common 'null is not an object' runtime errors in applications.
Optional chaining (?.) allows you to safely access nested properties without manually checking if each level exists. Nullish coalescing (??) provides a default value only when the left-side operand is null or undefined, unlike '||' which triggers for any falsy value like 0 or empty strings.
The 'readonly' modifier is used to make properties immutable after their initial assignment. It can be applied to interface properties or class members, preventing any code from reassigning the value. This is highly useful for defining constants or ensuring data integrity within complex objects.
Literal types allow you to specify exact values that a string, number, or boolean can hold. For example, a type 'Direction' could be restricted to only 'North', 'South', 'East', or 'West'. This provides powerful validation and auto-completion when combined with union types.
Type narrowing is the process where TypeScript refines a variable's type into a more specific one within a conditional block. This is achieved using 'type guards' like 'typeof', 'instanceof', or custom functions that return a type predicate using the 'is' keyword for safety.
The 'keyof' operator takes an object type and produces a string or numeric literal union of its keys. For example, if an interface has 'id' and 'name', keyof will produce the type 'id' | 'name'. It is essential for creating dynamic, type-safe lookup functions.
Functions & Parameters7
Optional parameters are defined using a question mark (?) after the parameter name (e.g., name?: string). Default parameters are assigned a value directly in the function signature (e.g., count = 0), which also allows TypeScript to infer the parameter's type automatically without explicit annotation.
Function overloading allows you to define multiple signatures for a single function, describing the different ways it can be called. You provide one or more 'overload signatures' followed by a single 'implementation signature' that is compatible with all the previous definitions to handle the logic.
Rest parameters allow a function to accept an indefinite number of arguments as an array. In TypeScript, you type them by following the spread syntax with an array type (e.g., ...numbers: number[]). This ensures all additional arguments passed to the function satisfy the specific type requirement.
Arrow functions are typed by specifying the parameter types and return type using the fat arrow syntax. For example: '(x: number, y: number) => number'. Alternatively, you can define a type alias for the function signature and apply it to the variable name for better readability.
In TypeScript, you can explicitly type the 'this' context of a function by making it the first parameter. This parameter is erased during compilation but allows the compiler to verify that the function is being called in a valid context with the expected object structure.
Generic functions use a type parameter (usually <T>) before the parentheses. For example: 'function identity<T>(arg: T): T { return arg; }'. This allows the function to work with any type while maintaining the relationship between the input type and the output type for the caller.
ReturnType is a built-in utility type that extracts the return type of a function type. It uses conditional types and the 'infer' keyword internally. It is particularly useful for getting the type of a complex function's output when that type isn't explicitly exported or defined.
Interfaces & Classes10
Optional properties in an interface are marked with a question mark (?) after their name. They indicate that an object implementing the interface may or may not contain those specific properties, giving flexibility while still allowing the compiler to check the types of those fields if present.
Declaration merging is a unique feature where the TypeScript compiler automatically combines two or more separate interfaces with the same name into a single definition. This allows developers to extend existing third-party library interfaces without modifying the original source code directly, enabling scalable type definitions.
Index signatures allow you to define the types for properties of an object when you don't know all the property names in advance. They use the syntax '[key: string]: string', which tells TypeScript that any property accessed on that object will return a value of the specified type.
'public' (default) allows access from anywhere. 'private' restricts access to only within the class itself. 'protected' allows access within the class and its subclasses. These modifiers help enforce encapsulation and control how class members are used across different parts of the application architecture.
TypeScript's 'private' keyword provides compile-time privacy, but the field is still accessible at runtime in JavaScript. The '#private' syntax is the native JavaScript private class feature, which provides hard runtime privacy, preventing any access from outside the class even through direct inspection or debugging tools.
Abstract classes serve as base classes that cannot be instantiated directly. They can contain 'abstract methods,' which have no implementation and must be defined in derived subclasses. This ensures that all subclasses follow a specific structure while allowing for custom behavior in each implementation.
'extends' is used for inheritance between classes, allowing a child to inherit logic and properties from a parent. 'implements' is used to force a class to follow the structure defined by an interface, requiring the class to define all properties and methods specified in that contract.
Parameter properties allow you to declare and initialize a class member in a single place: the constructor arguments. By prefixing a constructor parameter with an access modifier (like private or public), TypeScript automatically creates a property of that name and assigns the argument's value to it.
Static members belong to the class itself rather than to instances of the class. They are accessed using the class name (e.g., MyClass.count). They are useful for constants, factory methods, or tracking data that should be shared across all instances, such as a global counter.
Getters and setters are special methods that intercept access to a property. They allow you to add logic (like validation or formatting) when a value is retrieved or updated, while keeping the external API looking like a simple property access, following the encapsulation principle of OOP.
Advanced Types10
Utility types are built-in generic types that transform existing types into new ones. They simplify common scenarios: Partial makes properties optional, Required makes them mandatory, Pick selects specific keys, Omit removes specific keys, and Record creates an object type with specific keys and value types.
Partial<T> creates a new type where all properties of T are optional. It is frequently used in 'update' functions where a user might only want to change a few fields of an object at a time, ensuring that the function accepts a subset of the original structure.
Pick<T, K> creates a type by selecting only the set of properties K from type T. Omit<T, K> does the opposite, creating a type with all properties of T except those specified in K. These are essential for creating specialized views of large data objects.
Mapped types allow you to create new types by transforming each property in an existing type. Using a syntax similar to array mapping, you can iterate over keys (using 'in keyof') to change types, add modifiers like readonly, or make properties optional across an entire object.
Conditional types allow you to select one of two possible types based on a condition expressed as a type relationship (using 'T extends U ? X : Y'). This enables powerful meta-programming, allowing types to change dynamically based on the types passed as generic arguments.
The 'infer' keyword is used within a conditional type's 'extends' clause to declare a type variable that can be 'extracted' from another type. It allows the compiler to discover and name a specific component of a type, such as the item type within an array.
Discriminated unions are a pattern where multiple types in a union share a common literal property (the 'tag' or 'discriminant'). This allows TypeScript to perform exhaustive checks and narrowing, ensuring that you can safely access properties unique to a specific variant within a conditional block.
Custom type guards are functions that return a 'type predicate' (e.g., val is string). When used in a conditional statement, these functions tell the TypeScript compiler that if the function returns true, the variable must be of the specified type, enabling safe property access within that block.
The 'as const' assertion creates 'const' literal types for objects and arrays. It prevents widening (e.g., a string becomes a specific literal value rather than just 'string') and recursively makes all properties of an object 'readonly', providing the strictest possible type for a given constant value.
Tuple types allow you to express an array with a fixed number of elements where each element has a known, specific type. Unlike standard arrays, the order matters, and TypeScript will validate that each index contains the correct type (e.g., [string, number]).
Enums & Modules5
Enums allow you to define a set of named constants. Numeric enums automatically assign incrementing numbers starting from 0. String enums require each member to be initialized with a string literal, which provides better readability during debugging as the values are meaningful strings rather than obscure numbers.
Const enums are a performance optimization. Unlike regular enums, they are completely removed during compilation; their members are inlined at the call sites. This reduces the size of the generated JavaScript code by avoiding the creation of an actual object at runtime for the enum.
Modules use 'import' and 'export' and are the modern standard for organizing code, relying on the runtime's module loader. Namespaces are a legacy TypeScript feature for grouping code globally. In modern development, modules are almost always preferred for their isolation and compatibility with standard build tools.
Declaration files contain only type information and no executable code. They describe the 'shape' of existing JavaScript libraries to the TypeScript compiler, allowing you to use those libraries with full type safety, autocomplete, and documentation support without needing to rewrite them in TypeScript.
The 'declare' keyword is used to tell TypeScript that a variable, function, or class exists elsewhere (usually in a global JavaScript library or external file). It defines the type without generating any JavaScript code, acting as a 'promise' to the compiler that the value will be available.
Practical Questions3
To add TypeScript, first install the compiler and initialize a config using 'tsc --init'. Rename your .js files to .ts, and start by allowing JavaScript files in the config. Gradually add types to your code, resolving errors as you go until the project is fully typed and verified.
If a library lacks types, you can try installing types from the '@types' organization (DefinitelyTyped). If no types exist, you can create a shorthand declaration file (.d.ts) and 'declare module' for the library, setting it to 'any' to allow usage while you manually add specific types.
Common errors include 'Type X is not assignable to type Y' (fix by checking assignments), 'Property does not exist on type' (fix with interfaces/narrowing), and 'Object is possibly null' (fix with optional chaining or non-null assertions). Regularly checking compiler messages and using type guards resolves most issues.