30 TypeScript Interview Questions: From Basics to Advanced
Vitaliy Obolenskiy β Founder of Skillhacker Β· June 6, 2026
Test yourself with 30 carefully curated TypeScript questions β from fundamental concepts to advanced type-level reasoning. This collection includes 15 easy, 8 medium, and 7 hard questions covering all major TypeScript topics.
Topics covered: Interfaces and Types, Generics, Modules, Variables and Declarations, Type System, Functions, Type Guards.
Use this for self-assessment, team quizzes, or interview preparation. Each question includes a detailed answer breakdown.
π‘ Want to test your knowledge interactively? Try the full TypeScript assessment on Skillhacker β get instant feedback, track your progress, and compare your results with other developers.
π Table of Contents
Easy Questions
1. Using readonly to enforce immutability in interfaces
Topic: Interfaces and Types Β· Difficulty: β Easy
Given the TypeScript interface below, what happens when attempting to assign a new value to x after object creation?
interface Coordinate {
readonly x: number;
readonly y: number;
}
const c: Coordinate = { x: 5, y: 15 };
c.x = 20;
View Answer
The readonly modifier is a compile-time construct in TypeScript. Assigning to a readonly property triggers:
Error: TS2540: Cannot assign to 'x' because it is a read-only property.
Key points:
- It does not affect runtime behavior
- No JavaScript error is thrown unless additional runtime immutability (e.g.,
Object.freeze) is applied - It's purely for type-checking
2. Type parameter constraint with extends
Topic: Generics Β· Difficulty: β Easy
What does the extends keyword do in a generic type parameter declaration like <T extends string>?
View Answer
extends in generic constraints sets an upper bound: T must be assignable to the specified type.
Example:
function logLength<T extends string>(value: T): void {
console.log(value.length); // β
Safe to access .length
}
logLength("hello"); // β
OK
logLength(42); // β Error: number doesn't extend string
Valid types for T:
string'hello'(literal type)string & { length: number }
Invalid: number, boolean, etc.
This enables safe access to shared properties.
3. Purpose and characteristics of .d.ts files
Topic: Modules Β· Difficulty: β Easy
What is the primary purpose of a TypeScript declaration file (.d.ts)?
View Answer
Declaration files (.d.ts) contain only type information β no executable code.
Purpose:
- Enable type checking for libraries without built-in TypeScript support
- Provide autocompletion and IntelliSense in editors
- Describe the shape of JavaScript modules
Key characteristics:
- Not emitted as
.jsfiles - Have no runtime impact
- Used by TypeScript compiler and IDEs
4. Purpose of the 'declare' keyword
Topic: Variables and Declarations Β· Difficulty: β Easy
What is the primary purpose of the declare keyword in TypeScript variable declarations?
View Answer
declare is used in ambient declarations to inform TypeScript that a variable exists at runtime without generating JavaScript code.
Common use cases:
- External scripts or global scope variables
- Libraries loaded via
<script>tags - Global constants in
.d.tsfiles
Example:
declare const API_KEY: string;
// TypeScript knows API_KEY exists, but no JS is emitted
Key points:
- Has no runtime effect
- Purely for type-checking
- Commonly seen in
.d.tsfiles
5. TypeScript array typing and type safety enforcement
Topic: Type System Β· Difficulty: β Easy
Consider this code:
let items: string[] = ['a', 'b'];
items.push(42); // β error
Why does the second line produce a TypeScript error?
View Answer
The type annotation string[] means "array whose elements are all of type string".
What happens:
- TypeScript enforces this at compile time
- Calling
.push(42)violates the constraint because42is not assignable tostring - The error occurs during type checking, not at runtime
Important:
push()is fully supported onstring[]- The array is mutable (not readonly) by default
- Only the element type is restricted
6. Rest parameter type annotation
Topic: Functions Β· Difficulty: β Easy
How do you correctly type a rest parameter that accepts zero or more numbers?
View Answer
Rest parameters must be typed as an array:
// β
Correct
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// β Incorrect
function sum(...nums: number): number { ... }
Valid options:
...nums: number[](idiomatic, preferred)...nums: Array<number>(equivalent)
Invalid:
...nums: number(not an array type)[...number[]](tuple rest syntax, not for standalone rest parameters)
7. Literal types and type widening
Topic: Type System Β· Difficulty: β Easy
What is the difference between these two type declarations?
let a = "hello";
let b: string = "hello";
let c: "hello" = "hello";
View Answer
Key difference: literal types vs primitive types
let a = "hello":
- TypeScript infers the literal type
"hello"(withconstit stays literal; withlet, it widens tostring) - With
let,"hello"is widened tostringon reassignment analysis
let b: string:
- Type is
string(any string value) - Can be reassigned to any string
let c: "hello":
- Type is the literal type
"hello"(only this exact value) - Can only be
"hello", nothing else
Example:
let wide: string = "hello";
wide = "world"; // β
OK
let narrow: "hello" = "hello";
narrow = "world"; // β Error: Type '"world"' is not assignable to type '"hello"'
Why it matters:
- Literal types enable precise type constraints
- Essential for discriminated unions
- Powers type-safe enums and configuration objects
8. Invalid type guard example
Topic: Type Guards Β· Difficulty: β Easy
Why does the following function NOT act as a type guard?
function isNumber(val: any): boolean {
return typeof val === 'number';
}
View Answer
Although the function correctly checks typeof val === 'number', it returns boolean, not a type predicate.
Correct type guard:
function isNumber(val: any): val is number {
return typeof val === 'number';
}
Why it matters:
- Without
val is number, TypeScript won't narrowval's type in calling code - The function works at runtime but provides no compile-time type safety
9. When to prefer unknown over any for untrusted input
Topic: Type System Β· Difficulty: β Easy
You're parsing JSON from an external API whose shape is undocumented. Which type best expresses "this value exists but we don't know its structure β and we must check before use"?
View Answer
Answer: unknown
unknown is the type-safe counterpart to any:
- Represents a value whose type is truly unknown
- TypeScript requires type narrowing before accessing properties
- Prevents accidental misuse and catches errors at compile time
Example:
const data: unknown = JSON.parse(jsonString);
// β Error: Object is of type 'unknown'
console.log(data.name);
// β
OK: Narrow first
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name);
}
Why not other options:
anydisables type checking entirelyObjectis too vague and doesn't prevent unsafe accessRecord<string, unknown>is useful after narrowing to an object
10. Extending interfaces with extends keyword
Topic: Interfaces and Types Β· Difficulty: β Easy
Which TypeScript syntax correctly defines a new interface ColorSquare that extends the base interface Square and adds a color property?
View Answer
Correct syntax:
interface Square {
side: number;
}
interface ColorSquare extends Square {
color: string;
}
Key points:
extendsis used to inherit members from one or more base interfacesinheritsis not a valid TypeScript keywordimplementsis used by classes to satisfy an interface, not for interface extension
Note: While intersection types (&) can achieve similar structural results, they are not interface extension and don't support declaration merging.
11. Purpose of Partial utility type
Topic: Generics Β· Difficulty: β Easy
What does the Partial<Type> utility type do in TypeScript?
View Answer
Partial<Type> constructs a type where all properties of Type are marked as optional.
Example:
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
// Equivalent to:
// {
// name?: string;
// age?: number;
// email?: string;
// }
Use cases:
- Creating update or patch objects
- Optional configuration objects
- Partial form data
12. Barrel file purpose
Topic: Modules Β· Difficulty: β Easy
What is the main purpose of a barrel file (e.g., index.ts) in a TypeScript project?
View Answer
A barrel file (typically index.ts) re-exports key members from sibling modules.
Before barrel file:
import { A } from './utils/a';
import { B } from './utils/b';
import { C } from './utils/c';
After barrel file (utils/index.ts):
import { A, B, C } from './utils';
Benefits:
- Improves API surface control
- Reduces import path complexity
- Provides cleaner public API
13. Tuple destructuring and type safety
Topic: Variables and Declarations Β· Difficulty: β Easy
Given let record: [string, number] = ["Alice", 75000]; let [name, salary] = record;, what is the type of name after destructuring?
View Answer
Answer: string
Tuple destructuring preserves element types:
nameis inferred asstringsalaryis inferred asnumber
Example:
let record: [string, number] = ["Alice", 75000];
let [name, salary] = record;
name = true; // β Error: Type 'boolean' is not assignable to type 'string'
Key point: Destructuring does not widen or erase types.
14. Function type annotation placement
Topic: Functions Β· Difficulty: β Easy
In TypeScript, where is the return type annotation placed in a function declaration?
View Answer
The return type annotation appears after the parameter list and before the function body, separated by a colon.
Correct syntax:
function greet(name: string): string {
return `Hello ${name}`;
}
// Arrow function
const greet = (name: string): string => `Hello ${name}`;
Key points:
- Enforces that the function returns a value compatible with the declared type
- JSDoc comments can provide hints but do not enforce types like native annotations
15. Union types and type narrowing
Topic: Type System Β· Difficulty: β Easy
Given this code, how does TypeScript know what type value is inside each branch?
function format(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
} else {
return value.toFixed(2);
}
}
View Answer
TypeScript uses control flow analysis to narrow union types.
What happens:
- Initial type:
string | number - After
typeof value === "string": type narrows tostring - In the
elsebranch: type narrows tonumber(the remaining member)
Key points:
- TypeScript tracks type information through control flow
- Each branch sees only the narrowed type
- No explicit type assertions required
Other narrowing techniques:
// in operator β object unions
if ("swim" in pet) { /* Fish */ }
// Discriminated unions
type Result =
| { status: "success"; data: string }
| { status: "error"; message: string };
if (result.status === "success") {
result.data; // string
}
// Type predicates
function isString(x: unknown): x is string {
return typeof x === "string";
}
Medium Questions
16. Distinguishing optional parameters from union with undefined
Topic: Functions Β· Difficulty: ββ Medium
What key difference exists between function f(x?: string) and function f(x: string | undefined)?
View Answer
Key difference: call-site requirements
function f(x?: string):
f(); // β
Valid - argument is truly optional
f("hello"); // β
Valid
f(undefined); // β
Valid
function f(x: string | undefined):
f(); // β Error - parameter is required
f("hello"); // β
Valid
f(undefined); // β
Valid - must explicitly pass undefined
Important notes:
- Both accept
undefinedas a value - Only
?changes call-site requirements - Neither permits
nullunless explicitly added to the union strictNullChecksaffects howundefined/nullare treated in assignments, not parameter optionality
17. Object type literal vs structural typing
Topic: Interfaces and Types Β· Difficulty: ββ Medium
Consider this code:
let pt: { x: number; y: number } = { x: 10, y: 20 };
pt = { x: 5, y: 15, z: 30 }; // β Error?
Why does the second assignment fail?
View Answer
TypeScript applies excess property checking to object literals assigned to typed variables.
What happens:
- The literal must not contain properties not present in the target type
- This helps catch typos and unintended properties
Important distinction:
// Object literal - checked strictly
let pt: { x: number; y: number } = { x: 5, y: 15, z: 30 }; // β Error
// Variable assignment - excess properties allowed
let point3D = { x: 5, y: 15, z: 30 };
pt = point3D; // β
OK - structural typing
Why: Object literals are checked strictly to catch mistakes, while variables follow structural typing rules.
18. Narrowing union types with typeof vs in
Topic: Type Guards Β· Difficulty: ββ Medium
When would you prefer typeof x === 'string' over 'prop' in x for narrowing a union type?
View Answer
Use typeof for primitive types:
function process(value: string | number) {
if (typeof value === 'string') {
console.log(value.toUpperCase()); // β
value is string
} else {
console.log(value.toFixed(2)); // β
value is number
}
}
Use in for object types:
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim(); // β
animal is Fish
} else {
animal.fly(); // β
animal is Bird
}
}
Key difference:
typeofdistinguishes primitive types (string,number,boolean, etc.)indistinguishes object types by presence of a property- Using
inon primitives causes a compile-time error
19. Trade-off of third-party library integration
Topic: Modules Β· Difficulty: ββ Medium
When using a JavaScript library without built-in TypeScript support, what is a common challenge developers faceβand how is it typically mitigated?
View Answer
Challenge: Many npm packages lack bundled type definitions.
Solutions (in order of preference):
DefinitelyTyped (most common):
npm install --save-dev @types/lodashCustom declaration files:
// my-declarations.d.ts declare module 'some-library' { export function doSomething(): void; }Inline type assertions (last resort):
import lib from 'some-library'; const typedLib = lib as SomeInterface;
What NOT to do:
- Disabling type checking defeats the purpose
- Overusing
anyorunknowncasts undermines type safety - Rewriting libraries isn't required β TypeScript supports JS interop natively
20. Using 'keyof' with generic type parameter
Topic: Generics Β· Difficulty: ββ Medium
Given function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }, what does T[K] represent?
View Answer
T[K] is an indexed access type (also called a lookup type).
What it does:
- Evaluates to the type of the property named by
KinT - Because
Kis constrained tokeyof T, this is type-safe and precise
Example:
interface User {
name: string;
age: number;
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { name: "Alice", age: 30 };
const name = getProperty(user, 'name'); // type: string
const age = getProperty(user, 'age'); // type: number
Why it's powerful:
- Maintains type relationship between key and value
- Powers utility types like
Pick,Record,Omit
21. Effect of readonly on arrays in TypeScript
Topic: Variables and Declarations Β· Difficulty: ββ Medium
What happens when you try to call .push() on a variable typed as readonly string[]?
View Answer
Result: Compile-time error
const items: readonly string[] = ['a', 'b'];
items.push('c'); // β Error: Property 'push' does not exist on type 'readonly string[]'
Why:
- TypeScript enforces immutability at compile time for
readonly string[] - The
.push()method is not available on readonly array types - No runtime error occurs because the code doesn't compile
Key points:
readonlyis purely a compile-time construct- It has no runtime effect
- The array is still mutable at runtime if you bypass TypeScript
Alternative: Use ReadonlyArray<string> (equivalent)
22. Type assertion necessity with 'unknown'
Topic: Type System Β· Difficulty: ββ Medium
Why does the following code produce a TypeScript error?
let foo: unknown = "Hello";
let bar: string = foo;
View Answer
The unknown type is intentionally non-assignable to concrete types without narrowing.
Why:
- Enforces type safety when dealing with untrusted values
- Prevents accidental misuse
Solutions:
Type assertion:
let bar: string = foo as string;Control-flow narrowing:
if (typeof foo === 'string') { let bar: string = foo; // β OK }Type guard function:
function isString(val: unknown): val is string { return typeof val === 'string'; } if (isString(foo)) { let bar: string = foo; // β OK }
Key point: unknown forces you to prove the type before using it.
23. Difference between undefined and null in TypeScript
Topic: Type System Β· Difficulty: ββ Medium
With strictNullChecks enabled, why are undefined and null treated as distinct types β and how does TypeScript use that for narrowing?
View Answer
In TypeScript, undefined and null are separate types, not assignable to each other or to other types without an explicit union.
Under strictNullChecks:
let name: string = undefined; // β Error
let name: string = null; // β Error
let a: string | undefined = undefined; // β
OK
let b: string | null = null; // β
OK
let c: string | undefined = null; // β Error β null β undefined
TypeScript-specific behavior:
- Optional properties (
email?: string) implystring | undefined, notnull --strictNullChecksforces you to model absence explicitly in the type system- Narrowing treats them separately:
function greet(name: string | null | undefined) {
if (name === undefined) return "Hello, guest";
if (name === null) return "Hello, anonymous";
return `Hello, ${name.toUpperCase()}`; // name: string
}
undefined vs null in practice:
undefinedβ missing value, uninitialized variable, omitted optional propertynullβ intentional absence, often from APIs that returnnull
Key point: Unlike JavaScript, TypeScript's type checker rejects passing null where only undefined is expected (and vice versa) unless the union includes both.
Hard Questions
24. Type assertion vs interface casting
Topic: Interfaces and Types Β· Difficulty: βββ Hard
When working with a loosely typed API response, why is const user = data as UserInterface potentially unsafe compared to using a type guard or validation function?
View Answer
Type assertions like as UserInterface provide zero runtime safety.
What happens:
- They merely tell the compiler to treat the value as conforming to the interface
- No actual validation occurs
- Interfaces are purely compile-time constructs
Example of the problem:
interface User {
name: string;
age: number;
}
const data = await fetch('/api/user').then(r => r.json());
const user = data as User; // β Unsafe!
console.log(user.name.toUpperCase()); // π₯ Runtime error if name is missing
Safe alternatives:
Runtime validation (Zod):
import { z } from 'zod'; const UserSchema = z.object({ name: z.string(), age: z.number() }); const user = UserSchema.parse(data); // β Validates at runtimeType guard function:
function isUser(data: unknown): data is User { return ( typeof data === 'object' && data !== null && 'name' in data && 'age' in data ); }
Key point: Type assertions are for when you know more than the compiler. For untrusted data, use runtime validation.
25. Ambient module declaration vs. global augmentation
Topic: Modules Β· Difficulty: βββ Hard
Which scenario correctly uses an ambient module declaration (declare module 'x') instead of global augmentation (declare global { ... }) in TypeScript?
View Answer
Use declare module 'x' for:
- Describing the shape of a module
- Libraries lacking type definitions
- Module-scoped namespaces
Example:
// lodash.d.ts
declare module 'lodash' {
export function chunk<T>(array: T[], size?: number): T[][];
export function compact<T>(array: (T | null | undefined)[]): T[];
}
// Now you can import it
import { chunk } from 'lodash';
Use declare global for:
- Modifying global scope
- Adding methods to built-in types
- Extending global interfaces
Example:
declare global {
interface Array<T> {
customMethod(): void;
}
namespace NodeJS {
interface ProcessEnv {
API_KEY: string;
}
}
}
Key difference:
declare moduleis for module importsdeclare globalis for global scope modifications
26. Ambient declaration vs implementation in global scope
Topic: Variables and Declarations Β· Difficulty: βββ Hard
Which declaration correctly declares a globally accessible constant without causing a duplicate identifier error in a .ts file that may be included multiple times?
View Answer
Answer: declare const
Why:
- Introduces an ambient declaration
- Asserts existence without emitting JavaScript
- Avoids duplicate definition errors during compilation
Example:
// global.d.ts
declare const API_VERSION: string;
declare function initializeApp(): void;
Why not other options:
Plain
const:const API_VERSION = '1.0'; // β Emits JS, causes duplicationexport:export const API_VERSION = '1.0'; // β Creates module-scoped binding, not globalglobalThisassignment:globalThis.API_VERSION = '1.0'; // β Runtime-only, no type safety
Use cases:
- Global constants in declaration files
- Multi-entrypoint setups
- Legacy script concatenation (
--outFile)
27. Conditional types with infer keyword
Topic: Generics Β· Difficulty: βββ Hard
What does this conditional type do, and how does the infer keyword work?
type Awaited<T> = T extends Promise<infer U> ? U : T;
View Answer
This conditional type extracts the resolved type from a Promise.
How it works:
T extends Promise<infer U>β checks ifTis assignable toPromise<something>infer Utells TypeScript to infer the type parameter and bind it toU? U : Tβ ifTis aPromise, return the inferred typeU; otherwise returnTunchanged
Examples:
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<number>>; // number
type C = Awaited<string>; // string (not a Promise)
type D = Awaited<Promise<Promise<boolean>>>; // boolean (with TS 4.5+ recursive Awaited)
More examples with infer:
// Extract function return type
type ReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : T;
// Extract constructor instance type
type InstanceType<T> = T extends new (...args: never[]) => infer R ? R : never;
Key points:
infercan only be used in theextendsclause of conditional types- It creates a type variable that TypeScript infers from the pattern
- Essential for advanced type-level programming and built-in utility types
28. Type guard scope and control flow limitations
Topic: Type Guards Β· Difficulty: βββ Hard
Consider this function using a type guard:
function process(data: string | number): string {
if (typeof data === 'string') {
return data.toUpperCase();
}
// Outside the if-block, what is `data`'s type?
return data.toString();
}
Why does this code compile successfully despite data being used as number after the guard?
View Answer
Built-in type guards enable control flow analysis.
What happens:
- Inside the
ifblock:datais narrowed tostring - After the
ifblock: TypeScript narrows it to the remaining union member βnumber
Visual representation:
function process(data: string | number): string {
// data: string | number
if (typeof data === 'string') {
// data: string (narrowed)
return data.toUpperCase();
}
// data: number (narrowed to remaining type)
return data.toString();
}
Key points:
- This is not implicit widening or inference
- It's precise, branch-local narrowing
- Based on exhaustive checking of the union
- Works with
typeof,instanceof,in, and custom type guards
Limitation: Control flow analysis doesn't persist across function calls or complex control structures.
29. Mapped types with key remapping
Topic: Generics Β· Difficulty: βββ Hard
What does this mapped type do, and how does the as clause work?
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
View Answer
This mapped type creates getter methods with transformed property names.
How it works:
[K in keyof T]β iterates over all keys ofT;Kis each key in turnas \get${Capitalize<string & K>}`` β key remapping (TypeScript 4.1+)- Transforms each key name
Capitalizeis a built-in template literal typestring & KensuresKis treated as a string (not number or symbol)
: () => T[K]β each property becomes a function returning the original property type
Step-by-step for Person:
// K = "name" β key becomes "getName", value: () => string
// K = "age" β key becomes "getAge", value: () => number
type PersonGetters = {
getName: () => string;
getAge: () => number;
};
Other key remapping examples:
// Remove keys by filtering with never
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
// Prefix all keys
type Prefixed<T> = {
[K in keyof T as `prefix_${string & K}`]: T[K];
};
// Remap to optional
type PartialByKeys<T, K extends keyof T> = Omit<T, K> & {
[P in K as P]?: T[P];
};
Key points:
- Key remapping enables powerful type transformations
- Works with template literal types
- Can filter, rename, or compute new keys
- Foundation for advanced utility types beyond
Partial,Pick, andOmit
30. Rest parameters and tuple types in function signatures
Topic: Functions Β· Difficulty: βββ Hard
Given function foo(...args: [number, string?, boolean]): void {}, which call is invalid according to TypeScript's rest parameter and tuple type rules?
View Answer
The rest parameter ...args: [number, string?, boolean] is typed as a fixed-length tuple.
Valid calls:
foo(1); // β
OK - [number]
foo(1, "hello"); // β
OK - [number, string]
foo(1, "hello", true); // β
OK - [number, string, boolean]
foo(1, undefined, true); // β
OK - optional parameter
Invalid call:
foo(1, "hello", true, "extra"); // β Error - too many arguments
Why:
- TypeScript treats it as equivalent to
(arg0: number, arg1?: string, arg2: boolean) => void - Passing four arguments violates arity
- Rest parameters with tuple types do not accept extra elements
To accept variable length:
// Use variadic tuple with rest element
function bar(...args: [number, ...string[]]): void {}
bar(1, "a", "b", "c"); // β
OK
π― Ready to Test Yourself?
You've just reviewed 30 TypeScript questions covering everything from basic syntax to advanced type-level programming. But reading about TypeScript and actually using it under pressure are two different things.
Take the interactive assessment on Skillhacker:
- β±οΈ Timed mode β simulate real interview conditions
- π Instant feedback β see your score immediately
- π Compare your results β see how you stack up against other developers
- π Practice mode β learn at your own pace with detailed explanations
Start the TypeScript Assessment β