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.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
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.
thecodeforge.io
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 ───interfaceUserProfile {
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.interfaceUserProfile {
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 interfacetypeAdminUser = 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 ───functiongetUserDisplayName(user: UserProfile): string {
// TypeScript knows `user.username` exists because of the interface contractreturn `@${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
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 ───functiongetFirstItemUnsafe(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 ───interfaceIdentifiable {
id: number;
}
function findById<T extendsIdentifiable>(items: T[], targetId: number): T | undefined {
// The constraint `T extends Identifiable` guarantees `item.id` always existsreturn 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 ───interfaceApiResponse<TData> {
data: TData;
status: number;
message: string;
}
interfaceOrderSummary {
orderId: string;
total: number;
itemCount: number;
}
// This function returns a fully typed response — caller knows exactly what's insidefunctionmockFetchOrder(): 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
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.
thecodeforge.io
Typescript Interview Questions
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 ───functionparseConfigUnsafe(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 ───functionparseConfigSafe(rawInput: unknown): string {
// You MUST narrow the type before TypeScript lets you touch itif (
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 stringreturn (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 ───typePaymentMethod = 'credit_card' | 'paypal' | 'crypto';
functionprocessPayment(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;
thrownewError(`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 ───functionthrowCriticalError(message: string): never {
// A function that ALWAYS throws never returns normally.// TypeScript uses `never` to signal: execution ends here.thrownewError(`CRITICAL: ${message}`);
}
// TypeScript knows code after this call is unreachable// throwCriticalError('Database connection lost');
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 ───interfaceSuccessResponse {
kind: 'success';
data: { userId: number; username: string };
}
interfaceErrorResponse {
kind: 'error';
errorCode: number;
message: string;
}
interfaceLoadingResponse {
kind: 'loading';
progress: number; // 0-100
}
typeApiState = SuccessResponse | ErrorResponse | LoadingResponse;
// ─── Custom type guard using `is` keyword ───functionisSuccessResponse(response: ApiState): response is SuccessResponse {
// When this returns true, TypeScript narrows `response` to `SuccessResponse`// in the calling code — even outside this functionreturn response.kind === 'success';
}
// ─── Exhaustive handler using narrowing ───functionrenderApiState(state: ApiState): string {
// TypeScript tracks the type at each branchif (state.kind === 'loading') {
// TypeScript knows this is LoadingResponse — `progress` is availablereturn `Loading... ${state.progress}% complete`;
}
if (isSuccessResponse(state)) {
// TypeScript knows this is SuccessResponse — `data` is availablereturn `Welcome back, ${state.data.username}!`;
}
// TypeScript has narrowed: only ErrorResponse is left// `errorCode` and `message` are both safely accessible herereturn `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 ───classNetworkErrorextendsError {
constructor(public statusCode: number, message: string) {
super(message);
this.name = 'NetworkError';
}
}
functionhandleError(error: unknown): string {
if (error instanceofNetworkError) {
// TypeScript knows `error` has `statusCode` herereturn `Network failure [${error.statusCode}]: ${error.message}`;
}
if (error instanceofError) {
return `General error: ${error.message}`;
}
return'Unknown error occurred';
}
console.log(handleError(newNetworkError(503, 'Service unavailable')));
// Output: Network failure [503]: Service unavailable
console.log(handleError(newError('Something broke')));
// Output: General error: Something broke
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.
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.
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 tutorialtypeHandler<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
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) {
returnclass extends Base {
timestamp = new Date();
};
}
function Activable<TBase extends Constructor>(Base: TBase) {
returnclass extends Base {
isActive = false;
activate() { this.isActive = true; }
};
}
classUser {
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 / Aspect
any
unknown
Type safety
None — checking disabled
Full — must narrow before use
Can assign to typed variable
Yes, always
Only after narrowing
Autocomplete support
No — editor goes blind
Yes, after narrowing
Ideal use case
Migrating legacy JS (temporary)
External data — APIs, JSON.parse
Runtime crash risk
High — no compile-time checks
Low — forces validation first
Recommended in production
No — use as last resort
Yes — the safe default for unknown data
⚙ Quick Reference
12 commands from this guide
File
Command / Code
Purpose
TypeVsInterface.ts
interface UserProfile {
type vs interface
Generics.ts
function getFirstItemUnsafe(items: any[]): any {
Generics
UnknownAnyNever.ts
function parseConfigUnsafe(rawInput: any) {
unknown vs any vs never
TypeGuards.ts
interface SuccessResponse {
Type Guards & Narrowing
AnyPoison.ts
function parseConfig(raw: any): any {
The 'any' Escape Hatch
OptionalSmell.ts
interface User {
Optional Properties
ArrayTuples.ts
type Entry = (string | number)[];
Arrays
IndexSignature.ts
type UserCache = {
Index Signatures
FunctionVariance.ts
type Handler = (event: T) => void;
Contravariance
MixinsExample.ts
type Constructor = new (...args: any[]) => T;
Mixins
OverloadExample.ts
function greet(name: string): string;
Function Overloading
Installation.sh
node --version
Installing TypeScript
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?
Both describe object shapes, but interfaces support declaration merging (you can declare the same interface twice and TypeScript combines them) while types are closed once declared. Use interfaces for object contracts and class implementations; use types for unions, intersections, and primitive aliases.
Was this helpful?
02
What are TypeScript generics and when should I use them?
Generics are type placeholders that get resolved at call time. Use them whenever you want a function or class to work with multiple types but still preserve type information — for example, a reusable API response wrapper or a typed find-by-id utility. They're the alternative to any that doesn't throw away type safety.
Was this helpful?
03
Why is using `any` in TypeScript considered bad practice?
any disables TypeScript's type checker for that variable entirely — you get no autocomplete, no error detection, and no safety. It defeats the purpose of using TypeScript. Use unknown for genuinely uncertain types (forcing you to validate before use) or a proper generic if the type is known at call time.