Intermediate 11 min · March 06, 2026

TypeScript Interview — Unknown vs Any Production Gotcha

TypeScript's 'unknown' type forces type guards, avoiding runtime crashes.

N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.

Follow
Verified
production tested
June 10, 2026
last updated
1,554
articles · all by Naren
Quick Answer
  • any disables all type checking — it's a backdoor to plain JavaScript. Use it only for migration or third-party gaps.
  • unknown forces a type guard before use — the safe container for API responses, user input, or deserialized JSON.
  • never represents values that can never occur — used in exhaustive switch checks and functions that always throw.
  • Production rule default to unknown for external data; treat any as technical debt that must be repaid with a type guard.
✦ Definition~90s read
What is TypeScript Interview Questions?

TypeScript interview questions are not trivia—they are probes into your understanding of the type system's sharp edges. The core mechanic is that TypeScript adds a static type layer on top of JavaScript, catching type mismatches at compile time rather than runtime.

Imagine you're running a restaurant kitchen.

But the real test is how you handle the escape hatches: any and unknown. These two types define the boundary between type safety and chaos. any disables all type checking—it's a backdoor to plain JavaScript. unknown forces a type check before use—it's the safe container for values you don't know yet.

In practice, unknown is the correct choice for API responses, user input, or deserialized JSON. any should be reserved for migration pain or third-party library gaps. Teams that default to any lose the entire benefit of TypeScript: they ship bugs that the compiler could have caught.

The production insight is simple: any propagates silently; unknown forces you to prove safety at every access point. Senior engineers treat unknown as the default for external data and any as a debt that must be repaid with a type guard.

Plain-English First

Imagine you're running a restaurant kitchen. JavaScript is like having cooks who can grab ANY ingredient from ANY shelf with zero labels — fast, but chaotic. TypeScript is like labelling every container: 'this shelf is ONLY for spices, this one ONLY for sauces.' The kitchen still runs the same way at service time (it still compiles to JavaScript), but during prep you catch mistakes before a dish goes out wrong. That's TypeScript — a safety net that lives at development time, not runtime.

TypeScript has gone from a Microsoft experiment to the de facto standard for serious JavaScript projects. React, Angular, Node, and NestJS teams all default to it now — not because it's trendy, but because it catches entire categories of bugs before your code ships. If you're interviewing at any company with a mature frontend or full-stack team, TypeScript fluency isn't optional anymore.

The problem TypeScript solves is subtle but expensive: JavaScript's dynamic typing is powerful but it lets you pass a string where a number was expected, access a property that doesn't exist, or call undefined as a function — all silently, until your user hits the bug in production. TypeScript adds a compile-time type checker that acts like a code reviewer who never sleeps and never misses a thing.

By the end of this article you'll be able to explain the difference between type and interface, use generics to write reusable code without sacrificing type safety, reason about unknown vs any, and walk into an interview ready to answer follow-up questions that trip up even experienced developers.

Why 'What Is TypeScript Interview Questions?' Misses the Point

TypeScript interview questions are not trivia—they are probes into your understanding of the type system's sharp edges. The core mechanic is that TypeScript adds a static type layer on top of JavaScript, catching type mismatches at compile time rather than runtime. But the real test is how you handle the escape hatches: any and unknown. These two types define the boundary between type safety and chaos. any disables all type checking—it's a backdoor to plain JavaScript. unknown forces a type check before use—it's the safe container for values you don't know yet. In practice, unknown is the correct choice for API responses, user input, or deserialized JSON. any should be reserved for migration pain or third-party library gaps. Teams that default to any lose the entire benefit of TypeScript: they ship bugs that the compiler could have caught. The production insight is simple: any propagates silently; unknown forces you to prove safety at every access point. Senior engineers treat unknown as the default for external data and any as a debt that must be repaid with a type guard.

The `any` Leak
Assigning any to a variable poisons every downstream usage—TypeScript stops checking that entire branch. unknown forces a cast or type guard before use.
Production Insight
Teams using any for API responses lose compile-time validation; a renamed field in the backend silently becomes undefined in the frontend.
The symptom: runtime crashes at property access that TypeScript could have flagged as a missing property.
Rule of thumb: deserialize external data into unknown, then validate with a type guard or Zod schema before treating it as your domain type.
Key Takeaway
unknown is the safe default for any value whose type you don't control at compile time.
any is a type-system escape hatch that should be treated as technical debt with a clear removal plan.
A type guard or schema validation is the only bridge between unknown and your application types.
TypeScript: unknown vs any vs never Trinity THECODEFORGE.IO TypeScript: unknown vs any vs never Trinity Core type safety concepts and production gotchas any Disables type checking, escape hatch unknown Type-safe counterpart to any never Unreachable code, exhaustive checks Type Guards & Narrowing Refine unknown to specific types Optional Properties Silent undefined, common bug source ⚠ Using any bypasses all type safety Prefer unknown and narrow with type guards THECODEFORGE.IO
thecodeforge.io
TypeScript: unknown vs any vs never Trinity
Typescript Interview Questions

type vs interface — Which One Do You Actually Reach For?

This is the most common TypeScript interview question, and most candidates answer it wrong — not because they don't know the syntax, but because they can't explain the practical difference.

Both type and interface describe the shape of an object. They're interchangeable in most everyday cases, which is exactly why interviewers ask the question — they want to know if you understand the edges.

The critical difference is that interfaces are open — you can declare the same interface in multiple places and TypeScript merges them. This is called declaration merging. Libraries use this heavily: they ship a type definition and let you extend it in your own code without touching theirs. Types are closed — once declared, they're final.

Types win when you need union types, intersection types, or you're aliasing a primitive. You can't write interface ID = string | number — that's a type job. Interfaces win when you're modelling objects that will be implemented by classes or extended by library consumers.

The rule most senior devs follow: use interface for public API shapes and object contracts; use type for everything else.

TypeVsInterface.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ─── INTERFACE: open, mergeable, ideal for object contracts ───
interface UserProfile {
  id: number;
  username: string;
}

// Declaration merging — TypeScript merges both declarations into one.
// Try doing this with `type` and you'll get a duplicate identifier error.
interface UserProfile {
  email: string; // merged in — now UserProfile has id, username AND email
}

const currentUser: UserProfile = {
  id: 1,
  username: 'sarah_dev',
  email: 'sarah@example.com', // required because of the merged declaration
};

// ─── TYPE: closed, flexible, handles unions and primitives ───
type UserID = string | number; // can't do this with interface

type AdminUser = UserProfile & {
  permissions: string[]; // intersection type — combines UserProfile + new fields
};

const adminUser: AdminUser = {
  id: 99,
  username: 'admin_alex',
  email: 'alex@example.com',
  permissions: ['read', 'write', 'delete'],
};

// ─── Practical function using both ───
function getUserDisplayName(user: UserProfile): string {
  // TypeScript knows `user.username` exists because of the interface contract
  return `@${user.username} (${user.email})`;
}

console.log(getUserDisplayName(currentUser));
// Output: @sarah_dev (sarah@example.com)

console.log(`Admin permissions: ${adminUser.permissions.join(', ')}`);
// Output: Admin permissions: read, write, delete
Output
@sarah_dev (sarah@example.com)
Admin permissions: read, write, delete
Interview Gold:
When an interviewer asks 'type or interface?', say: 'I default to interface for object shapes because declaration merging makes them safer for team code and library extensions. I reach for type when I need a union, intersection, or primitive alias — things interfaces literally can't do.' That answer shows you know the trade-offs, not just the syntax.

Generics — Writing Code That Doesn't Throw Away Type Safety

Generics are the point where a lot of intermediate TypeScript developers get stuck. They understand the syntax but can't explain why you'd use them over just using any.

Here's the problem generics solve: you want to write a function that works with many types, but you want TypeScript to still know what type came out the other end. any throws away that knowledge. Generics preserve it.

Think of a generic as a placeholder that gets filled in at call time. When you call getFirstItem<string>(myArray), TypeScript locks in the type as string for that entire call. It checks inputs AND outputs against that locked-in type. With any, TypeScript shrugs and checks nothing.

In real codebases, generics appear constantly: API response wrappers, repository patterns, utility hooks in React, queue data structures, and caching layers. If you've used useState<User>() in React, you've already used generics.

The constraint syntax (<T extends SomeType>) is especially important — it says 'T can be anything, but it must have at least these properties'. This lets you write flexible utilities without giving up your safety net.

Generics.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// ─── The problem: without generics, you lose type info ───
function getFirstItemUnsafe(items: any[]): any {
  return items[0];
  // Caller gets `any` back — TypeScript can't help after this point
}

const maybeAString = getFirstItemUnsafe(['apple', 'banana']);
// maybeAString is typed as `any` — no autocomplete, no safety

// ─── The solution: generics preserve type information ───
function getFirstItem<T>(items: T[]): T | undefined {
  // T is a placeholder. TypeScript fills it in based on what you pass.
  return items.length > 0 ? items[0] : undefined;
}

const firstFruit = getFirstItem(['apple', 'banana', 'cherry']);
// TypeScript infers T = string, so firstFruit is typed as `string | undefined`
console.log(firstFruit?.toUpperCase()); // Output: APPLE — .toUpperCase() is valid!

const firstScore = getFirstItem([98, 76, 84]);
// TypeScript infers T = number, so firstScore is typed as `number | undefined`
console.log(firstScore?.toFixed(2)); // Output: 98.00

// ─── Generic with a constraint: T must have an `id` property ───
interface Identifiable {
  id: number;
}

function findById<T extends Identifiable>(items: T[], targetId: number): T | undefined {
  // The constraint `T extends Identifiable` guarantees `item.id` always exists
  return items.find((item) => item.id === targetId);
}

const products = [
  { id: 1, name: 'Laptop', price: 999 },
  { id: 2, name: 'Mouse', price: 29 },
  { id: 3, name: 'Keyboard', price: 79 },
];

const foundProduct = findById(products, 2);
// TypeScript knows foundProduct has `id`, `name`, AND `price` — full type preserved
console.log(foundProduct?.name);  // Output: Mouse
console.log(foundProduct?.price); // Output: 29

// ─── Real-world pattern: typed API response wrapper ───
interface ApiResponse<TData> {
  data: TData;
  status: number;
  message: string;
}

interface OrderSummary {
  orderId: string;
  total: number;
  itemCount: number;
}

// This function returns a fully typed response — caller knows exactly what's inside
function mockFetchOrder(): ApiResponse<OrderSummary> {
  return {
    data: { orderId: 'ORD-2024-001', total: 149.99, itemCount: 3 },
    status: 200,
    message: 'OK',
  };
}

const orderResponse = mockFetchOrder();
console.log(`Order ${orderResponse.data.orderId}: $${orderResponse.data.total}`);
// Output: Order ORD-2024-001: $149.99
Output
APPLE
98.00
Mouse
29
Order ORD-2024-001: $149.99
Watch Out:
A common interview trap: 'What's the difference between <T> and <T extends object>?' With just <T>, T could be a primitive (string, number, boolean). Adding extends object restricts T to object types only. If your function tries to access a property on T and T could be a primitive, TypeScript will warn you — rightfully so.

unknown vs any vs never — The Trinity That Defines TypeScript Maturity

Nothing separates a TypeScript beginner from an intermediate developer faster than how they use any. Beginners use it everywhere to 'shut TypeScript up'. Intermediate developers know it's a code smell. Senior developers understand when unknown and never are the correct tools instead.

any is an escape hatch. It turns off type checking entirely for that variable. The compiler will never complain, but you've also lost all the protection TypeScript offers. Using any on an API response is particularly dangerous — you're essentially pretending the data is safe when you haven't validated it.

unknown is the type-safe version of any. It says 'this value exists but I don't know what it is yet — and TypeScript won't let me use it until I prove what it is.' You must narrow it (with typeof, instanceof, or a type guard) before you can do anything with it. This is perfect for data coming from external sources: API responses, JSON parsing, user input.

never is the opposite end of the spectrum — it represents something that can never happen. It's the return type of a function that always throws, or the type of a variable in a branch that TypeScript has proven is unreachable. It's most powerful in exhaustive switch statements — if you add a new union member and forget to handle it, TypeScript will surface a never error that tells you exactly where.

UnknownAnyNever.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// ─── any: TypeScript goes completely silent — dangerous ───
function parseConfigUnsafe(rawInput: any) {
  // TypeScript won't stop you doing any of this, even if it crashes at runtime
  console.log(rawInput.settings.theme.toUpperCase()); // no error, even if undefined
}

// ─── unknown: safe alternative — must prove the type before using it ───
function parseConfigSafe(rawInput: unknown): string {
  // You MUST narrow the type before TypeScript lets you touch it
  if (
    typeof rawInput === 'object' &&
    rawInput !== null &&
    'theme' in rawInput &&
    typeof (rawInput as { theme: unknown }).theme === 'string'
  ) {
    // Now TypeScript is satisfied — we've proven `theme` is a string
    return (rawInput as { theme: string }).theme.toUpperCase();
  }
  return 'DEFAULT';
}

console.log(parseConfigSafe({ theme: 'dark' }));  // Output: DARK
console.log(parseConfigSafe({ theme: 42 }));       // Output: DEFAULT
console.log(parseConfigSafe(null));                // Output: DEFAULT

// ─── never: exhaustive checks — TypeScript tells you when you miss a case ───
type PaymentMethod = 'credit_card' | 'paypal' | 'crypto';

function processPayment(method: PaymentMethod): string {
  switch (method) {
    case 'credit_card':
      return 'Processing credit card charge...';
    case 'paypal':
      return 'Redirecting to PayPal...';
    case 'crypto':
      return 'Generating wallet address...';
    default:
      // This line is the safety net.
      // If you add 'bank_transfer' to PaymentMethod and forget to handle it,
      // TypeScript will error HERE because `method` would be `never` in the
      // default case — and `never` can't be assigned to `never`.
      const exhaustiveCheck: never = method;
      throw new Error(`Unhandled payment method: ${exhaustiveCheck}`);
  }
}

console.log(processPayment('paypal'));      // Output: Redirecting to PayPal...
console.log(processPayment('crypto'));      // Output: Generating wallet address...

// ─── never as a function return type ───
function throwCriticalError(message: string): never {
  // A function that ALWAYS throws never returns normally.
  // TypeScript uses `never` to signal: execution ends here.
  throw new Error(`CRITICAL: ${message}`);
}

// TypeScript knows code after this call is unreachable
// throwCriticalError('Database connection lost');
Output
DARK
DEFAULT
DEFAULT
Redirecting to PayPal...
Generating wallet address...
Interview Gold:
If an interviewer asks 'when would you use unknown over any?', say: 'Any time data comes from outside my control — API responses, JSON.parse results, or third-party events. unknown forces me to validate the data before using it, which is exactly the right behaviour. any would let me skip validation and ship a runtime crash.' That answer shows production thinking, not just syntax knowledge.

Type Guards & Narrowing — How TypeScript Actually Gets Smarter Mid-Function

Type narrowing is the mechanism that makes TypeScript feel intelligent. You start with a wide type — string | number | null — and through checks inside your function, TypeScript progressively narrows its understanding of what the value actually is.

This isn't magic — TypeScript reads your control flow and updates the type at each branch. After an if (typeof value === 'string') check, TypeScript knows inside that block that value is definitely a string. This is called control flow analysis.

Built-in narrowing uses typeof, instanceof, in, and equality checks. But the really powerful tool is the custom type guard — a function that returns value is SomeType. When that function returns true, TypeScript accepts the narrowing and updates the type in the calling scope.

Type guards are essential when working with discriminated unions — a pattern where each member of a union has a shared kind or type field that uniquely identifies it. This pattern is everywhere in Redux actions, API event types, WebSocket messages, and state machines.

TypeGuards.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// ─── Discriminated union: each type has a unique `kind` field ───
interface SuccessResponse {
  kind: 'success';
  data: { userId: number; username: string };
}

interface ErrorResponse {
  kind: 'error';
  errorCode: number;
  message: string;
}

interface LoadingResponse {
  kind: 'loading';
  progress: number; // 0-100
}

type ApiState = SuccessResponse | ErrorResponse | LoadingResponse;

// ─── Custom type guard using `is` keyword ───
function isSuccessResponse(response: ApiState): response is SuccessResponse {
  // When this returns true, TypeScript narrows `response` to `SuccessResponse`
  // in the calling code — even outside this function
  return response.kind === 'success';
}

// ─── Exhaustive handler using narrowing ───
function renderApiState(state: ApiState): string {
  // TypeScript tracks the type at each branch
  if (state.kind === 'loading') {
    // TypeScript knows this is LoadingResponse — `progress` is available
    return `Loading... ${state.progress}% complete`;
  }

  if (isSuccessResponse(state)) {
    // TypeScript knows this is SuccessResponse — `data` is available
    return `Welcome back, ${state.data.username}!`;
  }

  // TypeScript has narrowed: only ErrorResponse is left
  // `errorCode` and `message` are both safely accessible here
  return `Error ${state.errorCode}: ${state.message}`;
}

// ─── Testing all branches ───
const loadingState: ApiState = { kind: 'loading', progress: 45 };
const successState: ApiState = { kind: 'success', data: { userId: 7, username: 'maya_codes' } };
const errorState: ApiState = { kind: 'error', errorCode: 404, message: 'User not found' };

console.log(renderApiState(loadingState));  // Output: Loading... 45% complete
console.log(renderApiState(successState)); // Output: Welcome back, maya_codes!
console.log(renderApiState(errorState));   // Output: Error 404: User not found

// ─── instanceof guard — useful with class hierarchies ───
class NetworkError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

function handleError(error: unknown): string {
  if (error instanceof NetworkError) {
    // TypeScript knows `error` has `statusCode` here
    return `Network failure [${error.statusCode}]: ${error.message}`;
  }
  if (error instanceof Error) {
    return `General error: ${error.message}`;
  }
  return 'Unknown error occurred';
}

console.log(handleError(new NetworkError(503, 'Service unavailable')));
// Output: Network failure [503]: Service unavailable

console.log(handleError(new Error('Something broke')));
// Output: General error: Something broke
Output
Loading... 45% complete
Welcome back, maya_codes!
Error 404: User not found
Network failure [503]: Service unavailable
General error: Something broke
Pro Tip:
Discriminated unions with a kind or type field are the TypeScript equivalent of the strategy pattern. They're the go-to for modelling state machines, Redux actions, and WebSocket event types. If you model these correctly, TypeScript prevents you from ever accessing a field that doesn't exist on the current variant — zero runtime undefined errors.

The 'any' Escape Hatch — When You've Already Lost

Competitors treat 'any' like a type. It's not. It's a surrender flag. When you annotate a variable with 'any', you're telling TypeScript 'I don't care enough to figure this out, just get out of my way'. That's fine for a one-off migration script. It's a disaster in production.

The real problem is that 'any' is viral. One 'any' argument in a function poisons the return type. Suddenly your entire call chain is unchecked. I've debugged midnight outages caused by a single 'any' in an API client that swallowed a shape change.

You have two better options. Use 'unknown' when you genuinely don't know the shape at compile time — that forces you to validate before use. Use a branded type or union when you know the possible shapes. 'Any' is for the five minutes it takes you to write the proper type. After that, kill it.

AnyPoison.tsPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — interview tutorial

// DON'T: Surrender control
function parseConfig(raw: any): any {
  return raw; // no idea what this is
}

// DO: Force validation
function parseConfig(raw: unknown): Config {
  if (
    typeof raw !== 'object' ||
    raw === null ||
    !('apiKey' in raw) ||
    typeof raw.apiKey !== 'string'
  ) {
    throw new Error('Invalid config shape');
  }
  return { apiKey: raw.apiKey };
}

type Config = { apiKey: string };
Production Trap:
Every 'any' in a shared library is a landmine for the next dev. Enforce 'no-explicit-any' in CI and review exceptions like security incidents.
Key Takeaway
Never use 'any' in production code. Prefer 'unknown' with validation, or define the union explicitly.

Optional Properties — The Silent Undefined

Competitors ask 'Can we specify optional properties?' Yes. But the real question is: do you understand the downstream chaos they cause? An optional property is a contract that says 'this field might not exist'. That means every consumer needs a null check.

I've seen a junior add one optional field to a shared response type and break five screens. The worst part? No compile errors — because every consumer used optional chaining 'just in case'. The property was always present, but nobody knew that. The code was full of '?.'' checks that hid a design smell.

Better approach: model states as discriminated unions. If a user can have a 'billingAddress' or not, don't make it optional. Make the user type a union of 'HasBilling | NoBilling'. Now the compiler enforces the branching. Optional properties should be the exception, not the default.

OptionalSmell.tsPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// io.thecodeforge — interview tutorial

// MEDIOCRE: Silent undefined
interface User {
  name: string;
  billingAddress?: Address; // maybe
}

// BETTER: Explicity states
interface UserWithBilling {
  kind: 'with-billing';
  name: string;
  billingAddress: Address;
}

interface UserWithoutBilling {
  kind: 'no-billing';
  name: string;
}

type User = UserWithBilling | UserWithoutBilling;

// Compiler forces you to handle both cases
function sendInvoice(user: User) {
  if (user.kind === 'with-billing') {
    // user.billingAddress is guaranteed
    console.log(user.billingAddress.street);
  }
}
Senior Shortcut:
Before adding '?' to a property, ask: 'Should this be a different state in a discriminated union?' The extra initial effort pays off in every future PR.
Key Takeaway
Optional properties are a code smell for missing state modelling. Prefer unions for mutually exclusive states.

Arrays — More Than Just T[]

Competitors explain arrays with 'const arr: number[] = [1,2,3]'. That's the syntax. It's not the interesting part. The interesting part is what happens when you have a heterogeneous array or a read-only constraint.

In production payment systems, I've seen 'Array<any>' propagate because someone had mixed types and couldn't be bothered to write the tuple. That's where runtime crashes come from. If your array has a fixed structure — like a coordinate pair or a key-value entry — use a tuple: '[string, number]' not 'any[]'.

Also: prefer 'ReadonlyArray<T>' when a list shouldn't be mutated. It's a compile-time promise that prevents accidental 'push' or 'sort' calls. Combine it with 'as const' for literal tuples that are truly immutable. Every time I see a mutable array in a module that doesn't own the data, I know someone's going to get a surprise mutation bug.

ArrayTuples.tsPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — interview tutorial

// WRONG: lose all structure
type Entry = (string | number)[];
const bad: Entry = ['open', 42];
bad.push('unexpected'); // runtime ok, logically wrong

// RIGHT: tuple with named elements
type Entry = [status: string, count: number];
const good: Entry = ['open', 42];

// IMMUTABLE: no mutations allowed
const orders: ReadonlyArray<Entry> = [
  ['pending', 3],
  ['shipped', 12],
];
// orders.push(['cancelled', 1]); // compile error

// COMPILER-SAFE LITERAL TUPLE
const config = ['production', 8080] as const;
// config[1] = 9090; // compile error
Senior Shortcut:
Use tuples for any array with a known, fixed structure. Use 'ReadonlyArray' or 'as const' the moment you expose an array outside its creator.
Key Takeaway
Tuples replace 'any[]' for structured data. 'ReadonlyArray' prevents mutation bugs at compile time.

Index Signatures — When Your Object Keys Are Strangers

Index signatures are how you tell TypeScript 'I don't know the exact keys, but I know the shape of the values.' You see them everywhere in config maps, caches, and API response wrappers. The syntax is [key: string]: SomeType — and that key is never used at runtime, it's just a type hint for the signature.

The WHY: without index signatures, you'd be forced to use Record<string, any> or cast away safety. They let you model dynamic property access while keeping value types strict. But here's the trap — once you add an index signature, TypeScript assumes every property access returns that type. So obj.nonexistent won't error. That's why you pair them with undefined in the value type or use noUncheckedIndexedAccess in tsconfig.

In production, reach for index signatures when you're building lookup tables, normalized state stores, or any map where keys are user-generated. They're a clean escape for genuinely dynamic data — not a crutch for lazy typing.

IndexSignature.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — interview tutorial

type UserCache = {
  [userId: string]: { name: string; role: 'admin' | 'viewer' } | undefined;
};

const cache: UserCache = {};
cache['abc-123'] = { name: 'Alice', role: 'admin' };

// Safe access with undefined check
const user = cache['missing-id'];
console.log(user?.name ?? 'User not found');
Output
User not found
The Invisible Undefined Trap
Index signatures return SomeType, not SomeType | undefined. Turn on noUncheckedIndexedAccess or make undefined explicit in your value type — otherwise obj.misspelledKey silently compiles.
Key Takeaway
Index signatures model dynamic keys with known value shapes — always handle the missing-key case.

Contravariance — Why Your Callback Types Are Backwards

Function type variance answers: if a Cat is a subtype of Animal, is (cat: Cat) => void a subtype of (animal: Animal) => void? Intuitively no — because a function that expects a specific Cat shouldn't be passed a generic Animal. That's contravariance: function parameters go in the opposite direction of their types.

Return types are covariant: if the function returns Cat, it's safe to treat it as returning Animal. But parameters? They're contravariant. TypeScript historically allowed bivariance (both directions) for method signatures in classes — a known soundness hole. That's why --strictFunctionTypes exists and why you should write arrow functions in callbacks.

In reality, you hit this when defining event handlers or reducers. A function that handles MouseEvent can't safely replace one that handles Event — because the caller might pass a KeyboardEvent. Reverse the reasoning: a function accepting Event can stand in for one accepting MouseEvent. Checks out? Good. You're thinking contravariantly.

FunctionVariance.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — interview tutorial

type Handler<T> = (event: T) => void;

// Contravariant parameter: Handler<Event> is subtype of Handler<MouseEvent>
const handleAnyEvent: Handler<Event> = (e) => console.log(e.type);

const onlyMouse: Handler<MouseEvent> = handleAnyEvent; // OK

const onlyKeyboard: Handler<KeyboardEvent> = handleAnyEvent; // OK too

// Reverse fails: Handler<MouseEvent> is NOT subtype of Handler<Event>
// const bad: Handler<Event> = onlyMouse; // Compile error
Senior Shortcut:
When a type error says 'Argument of type X is not assignable to parameter of type Y' and the types look related, think variance — your callback parameter is too narrow. Widen the param type or add a generic constraint.
Key Takeaway
Function parameters are contravariant: a function accepting a wider type can replace one accepting a narrower type — never the other way.

Mixins — Composing Behavior Without Inheritance Chains

Mixins let you compose reusable behaviors from multiple classes into a single class, avoiding deep inheritance hierarchies. TypeScript doesn't have built-in mixin syntax like some languages, but you can implement them using constructor functions and class expressions. The core pattern: define a function that takes a base class and returns a new class extending it with added methods. You chain these functions to compose behavior. This bypasses single-inheritance limits and keeps code DRY. Why? Because inheritance chains become brittle as requirements grow; mixins let you mix and match capabilities like logging, serialization, or validation without coupling. The cost: TypeScript loses perfect type inference for mixins — you often need to explicitly annotate return types or use helper types. Never overuse mixins; they obscure control flow. Production reality: mixins solve cross-cutting concerns better than deep inheritance, but prefer composition via dependency injection when the behavior needs runtime substitution.

MixinsExample.tsPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — interview tutorial

type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = new Date();
  };
}

function Activable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActive = false;
    activate() { this.isActive = true; }
  };
}

class User {
  constructor(public name: string) {}
}

const ActiveTimedUser = Activable(Timestamped(User));
const u = new ActiveTimedUser('Alice');
u.activate();
console.log(u.name, u.isActive, u.timestamp);
Output
Alice true 2025-04-01T12:00:00.000Z
Production Trap:
Mixin output types lose visibility of added members. You must explicitly type the returned class or use intersection types to avoid 'Property does not exist' errors in consuming code.
Key Takeaway
Mixins compose behavior without inheritance chains, but sacrifice type inference — always annotate return types.

Function Overloading — TypeScript’s Static-Only Solution to JS Limitations

JavaScript doesn't support function overloading — you can't have multiple functions with the same name differing by parameters. TypeScript solves this at compile time with overload signatures. You write multiple function signatures (no bodies) above a single implementation signature with a union-typed body. The overloads tell TypeScript which argument patterns are valid; the implementation handles all cases. Why does this matter? It gives callers precise return types based on inputs, while the runtime keeps JavaScript's single-function reality. Only the implementation signature is compiled. Overloads are pure type-system decoration. Never use overloads for fundamentally different behaviors — that signals a design problem. Real-world use: parsing functions, API clients, or event handlers where argument shape changes return type. The trap: overloads don't throw runtime errors on invalid inputs — you must validate manually. Production rule: 3 overloads max; beyond that, refactor to generics or union types.

OverloadExample.tsPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — interview tutorial

function greet(name: string): string;
function greet(name: string, age: number): string;
function greet(name: string, age?: number): string {
  if (age !== undefined) {
    return `Hello, ${name}. You are ${age}.`;
  }
  return `Hello, ${name}.`;
}

console.log(greet('Alice'));
console.log(greet('Bob', 30));
Output
Hello, Alice.
Hello, Bob. You are 30.
Production Trap:
Overloads only provide compile-time safety. The implementation signature must handle all cases — a missing check produces silent undefined or runtime errors.
Key Takeaway
Function overloading is compile-time only; use overloads for precise return types, but always validate inputs in the implementation body.

Installing TypeScript — The Minimum You Actually Need

To write and run TypeScript, you need Node.js (version 14 or later) and npm (bundled with Node). The minimum install is one command: npm install -g typescript. This gives you the tsc compiler. Verify with tsc --version. That's it — no extra tooling required. You can compile a single file: tsc hello.ts outputs hello.js. Why this matters: most tutorials assume you need tsconfig.json, ts-node, or build tools. You don't. For production, you'll want a tsconfig.json to control strictness, module resolution, and output targets. But the interview answer: Node.js + npm + typescript package. Common trap: installing TypeScript globally vs locally. Global is fine for quick tests; projects should use local installs (npm install --save-dev typescript) to lock versions. Minimum requirements: Node 14+ (Node 18+ recommended for modern features), npm, and typescript. No editors, no bundlers, no frameworks. Just compile and run.

Installation.shPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — interview tutorial

# Check Node.js version (must be >=14)
node --version

# Install TypeScript globally
npm install -g typescript

# Verify installation
tsc --version

# Compile and run
cat > hello.ts << 'EOF'
let message: string = 'Hello TypeScript';
console.log(message);
EOF
tsc hello.ts && node hello.js
Output
v18.19.0
5.4.2
Hello TypeScript
Production Trap:
Global TypeScript installs can break CI pipelines. Always prefer local installs per project with exact versions pinned in package.json to avoid 'works on my machine' bugs.
Key Takeaway
Node.js 14+ and npm with npm install typescript is the minimum — no tsconfig or bundler required to start compiling.
Feature / Aspectanyunknown
Type safetyNone — checking disabledFull — must narrow before use
Can assign to typed variableYes, alwaysOnly after narrowing
Autocomplete supportNo — editor goes blindYes, after narrowing
Ideal use caseMigrating legacy JS (temporary)External data — APIs, JSON.parse
Runtime crash riskHigh — no compile-time checksLow — forces validation first
Recommended in productionNo — use as last resortYes — the safe default for unknown data

Key takeaways

1
Use interface for object shapes that classes implement or libraries extend
declaration merging is a feature, not a bug. Use type for unions, intersections, and primitive aliases.
2
Generics preserve type information across function boundaries
they're the difference between 'I know this returns a string' and 'I have no idea what this returns'. Never use any where a generic would work.
3
unknown is any with a safety lock
the correct default for data from external sources. TypeScript forces you to validate it before use, which is exactly what should happen with untrusted data.
4
Discriminated unions with a shared kind field turn TypeScript's control flow analysis into a compile-time state machine validator
add a new state, TypeScript tells you every switch that needs updating.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between type and interface in TypeScript?
02
What are TypeScript generics and when should I use them?
03
Why is using `any` in TypeScript considered bad practice?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.

Follow
Verified
production tested
June 10, 2026
last updated
1,554
articles · all by Naren
🔥

That's JavaScript Interview. Mark it forged?

11 min read · try the examples if you haven't

Previous
Node.js Interview Questions
5 / 5 · JavaScript Interview
Next
System Design Interview Guide