TypeScript adoption has grown to 78% among JavaScript developers, yet the language's most powerful features—never, infer, conditional types—remain a mystery to most. In technical screens, candidates can typically explain what interface and type do, but fewer than 30% can explain when the never type is actually useful.
Table of Contents
- Any vs Unknown Questions
- Never Type Questions
- Infer Keyword Questions
- Mapped Type Modifier Questions
- Generic Constraint Questions
- Conditional Type Distribution Questions
- Readonly Array vs Tuple Questions
- Quick Reference
Any vs Unknown Questions
These questions test your understanding of TypeScript's top types and type safety.
What is the difference between any and unknown in TypeScript?
Both any and unknown can accept any value. The difference is what you can do afterward. With any, TypeScript throws up its hands and says "do whatever you want—I'm not checking." With unknown, TypeScript says "I don't know what this is, so prove it before you use it."
function processValue(value: any): string {
return value.toUpperCase();
}
function processValueSafely(value: unknown): string {
return value.toUpperCase(); // Error!
}Think of any as turning off the alarm system. Think of unknown as saying "something entered the building—verify their identity before letting them into secure areas."
The unknown type was added in TypeScript 3.0 specifically to address the type safety issues with any. It's the type-safe counterpart that preserves type checking by requiring narrowing.
How do you safely use unknown values?
The safe version requires type narrowing before use. You must prove to TypeScript what the type actually is:
function processValueSafely(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase(); // Now TypeScript knows it's a string
}
throw new Error('Expected a string');
}When would you actually use any?
The any type has legitimate uses—migrating JavaScript codebases, dealing with truly dynamic data where exhaustive type guards aren't practical, or temporarily bypassing type errors during prototyping. But the key is treating it as a last resort, not a default.
Never Type Questions
These questions test your understanding of the bottom type and exhaustiveness checking.
What is the never type and when is it useful?
The never type represents the impossible—a value that can never exist. It's the "bottom type" in TypeScript's type hierarchy, meaning nothing can be assigned to it except another never.
Common uses include functions that always throw errors, infinite loops, exhaustive switch statement checks, and filtering types in conditional types. It's the opposite of any.
How do you use never for exhaustive type checks?
This pattern catches missing switch cases at compile time:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
default:
const exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}This code has a bug—we forgot to handle the 'triangle' case. TypeScript catches it at compile time because after the 'circle' and 'square' cases, the only remaining possibility for shape is the triangle variant. When we try to assign this non-never value to a variable of type never, TypeScript errors: "Type triangle is not assignable to type 'never'."
If our switch was truly exhaustive, shape would have no possible values left in the default case, and assigning "nothing" to never is fine. This pattern is particularly valuable when unions grow over time—adding a new shape variant to the union will immediately surface any switch statements that need updating.
Infer Keyword Questions
These questions test whether you've worked with advanced conditional types.
How does the infer keyword work in conditional types?
The infer keyword lets you "capture" a type from within a conditional type. It's like a type-level variable that gets its value from pattern matching.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number): string {
return `Hello ${name}, you are ${age}`;
}
type GreetReturn = ReturnType<typeof greet>; // string
type GreetParams = Parameters<typeof greet>; // [string, number]In T extends (...args: any[]) => infer R ? R : never:
- We check if
Tmatches the function pattern - If it does,
Rcaptures whatever the return type is - We then return that captured
R
Think of it like destructuring, but for types. Just as const { name } = user extracts the name property from an object, infer R extracts a type from within another type's structure.
How do you extract types from Promises using infer?
The infer keyword can extract the element type from a Promise:
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>; // Promise<number>
// Recursive version for nested promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type C = DeepAwaited<Promise<Promise<number>>>; // numberThe infer keyword can only appear in the "extends" clause of a conditional type and introduces a type variable that's only in scope within the "true" branch.
Mapped Type Modifier Questions
These questions test understanding of mapped types and their modifiers.
What do the plus and minus signs do in mapped types?
In mapped types, you can add or remove modifiers using + and -. The + is implied when you write readonly or ?, but the - explicitly removes them.
interface User {
readonly id: number;
name: string;
email?: string;
}
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
type MutableUser = Mutable<User>;
// { id: number; name: string; email?: string }
type RequiredUser = Required<User>;
// { readonly id: number; name: string; email: string }So -readonly strips the readonly modifier from all properties, and -? makes optional properties required. This is how TypeScript's built-in Required<T> utility type works.
How do you combine multiple modifiers in mapped types?
You can combine modifiers to transform multiple property characteristics at once:
type Writeable<T> = {
-readonly [K in keyof T]-?: T[K];
};
// Makes everything mutable AND required
type FullUser = Writeable<User>;
// { id: number; name: string; email: string }The modifier syntax (+readonly, -readonly, +?, -?) allows precise control over property characteristics in type transformations.
Generic Constraint Questions
These questions reveal whether you understand how generic constraints interact with type inference.
How do generic constraints preserve specific types?
The constraint K extends keyof T ensures that K can only be a valid key of T. But the return type T[K] is where the magic happens—it uses indexed access types to return the specific type of that property.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30, active: true };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
const active = getProperty(user, 'active'); // boolean
const invalid = getProperty(user, 'email'); // Error!TypeScript tracks which specific key was passed and preserves the precise return type.
Why does indexed access typing matter for return types?
Without the generic K, you'd have to return a union of all possible property types:
function getPropertyLoose<T>(obj: T, key: keyof T): T[keyof T] {
return obj[key];
}
const nameLoose = getPropertyLoose(user, 'name'); // string | number | booleanThe generic version preserves the precise type because TypeScript tracks which specific key was passed.
How do structural constraints work with generics?
Constraints are checked structurally, not nominally:
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Works fine
const result = merge({ name: 'Alice' }, { age: 30 });
// Also works - constraints are structural
const withArray = merge({ items: [1, 2, 3] }, { count: 3 });Constraints restrict what can be passed but don't affect inference within those bounds.
Conditional Type Distribution Questions
These questions catch many experienced developers off guard.
Why do conditional types distribute over unions?
Conditional types distribute over union types when the checked type is a naked type parameter. This means the conditional is applied to each union member separately:
type ToArray<T> = T extends any ? T[] : never;
type Result1 = ToArray<string>; // string[]
type Result2 = ToArray<string | number>; // string[] | number[]
// Wait, why isn't Result2 (string | number)[]?ToArray<string | number> isn't evaluated as one check. Instead, TypeScript applies the conditional to each union member separately:
ToArray<string>→string[]ToArray<number>→number[]- Combine:
string[] | number[]
How do you prevent conditional type distribution?
To prevent distribution and get (string | number)[], wrap the type parameter in a tuple:
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Result3 = ToArrayNonDistributive<string | number>; // (string | number)[]The brackets don't change the condition—[T] extends [any] is still always true. But wrapping in a tuple prevents the distribution behavior because the type parameter is no longer "naked" in the extends clause.
Readonly Array vs Tuple Questions
These questions test understanding of TypeScript's array and tuple types.
Why can't readonly arrays be passed where mutable arrays are expected?
as const does two things: it makes the array readonly and converts it to a tuple type with literal values. TypeScript won't let you pass a readonly array where a mutable one is expected because the function could theoretically modify it.
const arr1 = [1, 2, 3]; // number[]
const arr2 = [1, 2, 3] as const; // readonly [1, 2, 3]
function sum(numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
sum(arr1); // Works
sum(arr2); // Error: readonly [1, 2, 3] not assignable to number[]The issue is that number[] implies the array could be mutated (you could push, pop, etc.), but readonly [1, 2, 3] guarantees immutability.
How do you fix readonly array assignment errors?
The fix is to declare the parameter as readonly:
function sum(numbers: readonly number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
sum(arr1); // Works - mutable arrays are assignable to readonly
sum(arr2); // Works nowA mutable array can be passed where a readonly one is expected (widening is safe), but not vice versa. The as const assertion creates the most specific type possible—both immutable and tuple-typed—and readonly parameters are more flexible for function signatures.
Quick Reference
| Concept | Key Points |
|---|---|
| any vs unknown | any disables type checking; unknown requires narrowing before use |
| never | Bottom type for exhaustive checks and impossible values |
| infer | Pattern matching to capture types in conditional types |
| Mapped modifiers | -readonly and -? remove modifiers; + adds them |
| Generic constraints | K extends keyof T preserves specific key types |
| Distribution | Conditionals distribute over unions; wrap in [T] to prevent |
| readonly arrays | Can't assign to mutable array params; use readonly T[] |
Key mental models:
- Type hierarchy:
neveris at the bottom (nothing assignable to it),unknownis at the top (everything assignable to it). Everything else sits in between based on structural compatibility. - Variance: Function parameters are contravariant, return types are covariant. This is why
readonlyarrays aren't assignable to mutable array parameters. - Distribution: Conditional types distribute over unions when the type parameter appears "naked" in the extends clause.
- Never for filtering: Beyond exhaustiveness checks,
neveris useful in conditional types for filtering:type NonFunction<T> = T extends Function ? never : Tremoves function types from unions.
Related Articles
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- TypeScript Type vs Interface - When to use type aliases vs interfaces
- TypeScript Generics Interview Guide - Master generic types, constraints, and utility types
- 15+ Tricky JavaScript Interview Questions - The gotchas that catch most candidates off guard
Related Articles
If you found this helpful, check out these related guides:
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- TypeScript Type vs Interface - When to use type aliases vs interfaces
- TypeScript Generics Interview Guide - Master generic types, constraints, and utility types
- 12 Tricky JavaScript Interview Questions - The gotchas that catch most candidates off guard
