TypeScript interview questions
type vs interface
Both define the shape of an object, but have different capabilities.
Key differences
typeinterfaceObject shape Yes Yes Primitives/unions Yes No Declaration merging No Yes Extends Via &Via extendsMapped types ( [K in keyof T])Yes No Declaration merging (interface only)
tsinterface User { name: string; } interface User { age: number; } // result: { name: string; age: number; }Union types (type only)
tstype ID = string | number; type Status = 'active' | 'inactive' | 'banned';Extending
ts// interface interface Animal { name: string; } interface Dog extends Animal { breed: string; } // type type Animal = { name: string; }; type Dog = Animal & { breed: string; };Rule of thumb: use
interfacefor object shapes that may be extended or merged (e.g. library public APIs). Usetypefor unions, intersections, and utility types.updated
did you know it?Generics
Generics allow you to write reusable, type-safe code that works with different types without losing type information.
Without generics
ts// loses type information function identity(value: any): any { return value; }With generics
tsfunction identity<T>(value: T): T { return value; } const str = identity('hello'); // type: string const num = identity(42); // type: numberGeneric constraints
tsfunction getLength<T extends { length: number }>(value: T): number { return value.length; } getLength('hello'); // 5 getLength([1, 2, 3]); // 3Generic interfaces and types
tsinterface ApiResponse<T> { data: T; status: number; message: string; } type UserResponse = ApiResponse<User>; type ListResponse = ApiResponse<User[]>;Generics are the foundation of reusable utilities like
Array<T>,Promise<T>,Record<K, V>- they are everywhere in TypeScript.updated
did you know it?Utility Types
TypeScript ships with built-in generic types that transform existing types.
Most commonly used
tsinterface User { id: number; name: string; email: string; password: string; }Partial<T>- makes all properties optionaltsfunction updateUser(id: number, patch: Partial<User>) {}Required<T>- makes all properties requiredtstype StrictUser = Required<User>;Pick<T, K>- selects a subset of propertieststype PublicUser = Pick<User, 'id' | 'name'>;Omit<T, K>- removes specific propertieststype CreateUser = Omit<User, 'id'>;Readonly<T>- makes all properties read-onlytsconst config: Readonly<Config> = { ... };Record<K, V>- constructs an object typetstype RoleMap = Record<'admin' | 'user' | 'guest', string[]>;ReturnType<T>- extracts the return type of a functiontsfunction getUser() { return { id: 1, name: 'Alice' }; } type LoadedUser = ReturnType<typeof getUser>; // { id: number; name: string }Awaited<T>- unwraps a Promise typetstype Data = Awaited<ReturnType<typeof fetchUsers>>; // User[] instead of Promise<User[]>PartialandOmitare probably the most used in day-to-day work - especially when typing form state and API payloads.updated
did you know it?unknown vs any
Both represent "any possible value" but with very different safety guarantees.
any - turns off type checking
tslet value: any = 'hello'; value.toUpperCase(); // ok value.nonExistentMethod(); // ok - no error, but crashes at runtime value = 42; // okanyis a complete escape hatch - TypeScript stops checking entirely.unknown - type-safe alternative
tslet value: unknown = 'hello'; value.toUpperCase(); // Error - can't use without narrowing // you must narrow the type first if (typeof value === 'string') { value.toUpperCase(); // ok }Comparison
anyunknownAssignable to anything Yes No Operations without check Yes No Type narrowing required No Yes Safety None Full When to use
ts// any - only when migrating JS code or in tests const legacy: any = getLegacyData(); // unknown - for external data (API, JSON.parse, try/catch) try { await riskyOperation(); } catch (error: unknown) { if (error instanceof Error) { console.log(error.message); } }Prefer
unknownoveranywhenever possible - it forces you to handle all cases explicitly.updated
did you know it?Conditional Types
Conditional types select a type based on a condition, using the syntax
T extends U ? X : Y.Basic example
tstype IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // falseWith infer
inferlets you extract and capture a type from within a conditional.ts// extract the return type manually (like ReturnType<T>) type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function greet(): string { return 'hello'; } type Result = MyReturnType<typeof greet>; // stringDistributive conditional types
When
Tis a union, the condition is applied to each member.tstype NonNullable<T> = T extends null | undefined ? never : T; type A = NonNullable<string | null | undefined>; // stringPractical example
tstype Flatten<T> = T extends Array<infer Item> ? Item : T; type Str = Flatten<string[]>; // string type Num = Flatten<number>; // numberConditional types are the basis for many built-in utility types:
NonNullable,ReturnType,Parameters,Awaited.updated
did you know it?extends vs implements
extends - inheritance
A class inherits implementation (properties and methods) from a parent class.
tsclass Animal { constructor(public name: string) {} speak(): string { return `${this.name} makes a sound`; } } class Dog extends Animal { speak(): string { return `${this.name} barks`; } }implements - contract
A class promises to satisfy a type shape - it must provide its own implementation.
tsinterface Serializable { serialize(): string; deserialize(data: string): void; } class User implements Serializable { serialize(): string { return JSON.stringify(this); } deserialize(data: string): void { Object.assign(this, JSON.parse(data)); } }Using both together
tsclass AdminUser extends User implements Serializable, Loggable { // inherits from User, satisfies both interfaces }Key difference
extendsimplementsInherits implementation Yes No Multiple No (one class) Yes (many interfaces) Used with A class (or an interface extending an interface) Interfaces and object types implementswith an interface is a great way to enforce a contract across unrelated classes.updated
did you know it?Mapped Types
Mapped types create new types by iterating over the keys of an existing type.
ts// make all properties optional (like Partial<T>) type Optional<T> = { [K in keyof T]?: T[K]; }; // make all properties nullable type Nullable<T> = { [K in keyof T]: T[K] | null; };With remapping via as
tstype Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; type User = { name: string; age: number }; type UserGetters = Getters<User>; // { getName: () => string; getAge: () => number }Template Literal Types
Build string types by combining other types.
tstype Direction = 'top' | 'right' | 'bottom' | 'left'; type CSSPadding = `padding-${Direction}`; // 'padding-top' | 'padding-right' | 'padding-bottom' | 'padding-left'Practical example
tstype EventName<T extends string> = `on${Capitalize<T>}`; type ClickEvent = EventName<'click'>; // 'onClick' type ChangeEvent = EventName<'change'>; // 'onChange'Together mapped types and template literals allow generating entire API surfaces from a single source of truth.
updated
did you know it?Type Narrowing
Narrowing is the process of refining a broad type to a more specific one based on runtime checks. TypeScript understands these checks and adjusts the type accordingly.
typeof guard
tsfunction process(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); // string here } return value.toFixed(2); // number here }instanceof guard
tsfunction handleError(error: unknown) { if (error instanceof Error) { console.log(error.message); // Error here } }in operator
tstype Cat = { meow(): void }; type Dog = { bark(): void }; function makeSound(animal: Cat | Dog) { if ('meow' in animal) { animal.meow(); // Cat here } else { animal.bark(); // Dog here } }Discriminated unions
tstype Shape = | { kind: 'circle'; radius: number } | { kind: 'rect'; width: number; height: number }; function area(shape: Shape) { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'rect': return shape.width * shape.height; } }Type predicates
tsfunction isString(value: unknown): value is string { return typeof value === 'string'; }Discriminated unions + exhaustive switch is the most powerful narrowing pattern - TypeScript will warn you if a case is missing.
updated
did you know it?Declaration Files (.d.ts)
Declaration files describe the types of existing JavaScript code without containing any implementation.
What they contain
ts// types/analytics.d.ts declare function trackEvent(name: string, data?: object): void; declare const VERSION: string; declare module 'some-js-library' { export function doSomething(value: string): number; export interface Options { timeout: number; retries: number; } }When you need them
- Third-party JS libraries without built-in types (installed from
@types/...). - Your own JS code that you want to use in a TS project.
- Global variables injected by scripts (e.g.
window.analytics). - Ambient module declarations for non-TS files (CSS, SVG imports).
@types packages
bashnpm install -D @types/lodash npm install -D @types/nodeThese are community-maintained declaration files from the DefinitelyTyped repository.
Augmenting existing types
ts// extend the global Window interface declare global { interface Window { analytics: Analytics; } }When you install a library and TypeScript says
Could not find a declaration file- you either need@types/...or to write a.d.tsyourself.updated
did you know it?- Third-party JS libraries without built-in types (installed from
The satisfies operator
Introduced in TypeScript 4.9,
satisfiesvalidates that a value matches a type without widening the inferred type.Problem with regular annotation
tstype Colors = 'red' | 'green' | 'blue'; type Palette = Record<Colors, string | number[]>; // with annotation - TypeScript widens to string | number[] const palette: Palette = { red: '#ff0000', green: [0, 255, 0], blue: '#0000ff', }; palette.red.toUpperCase(); // Error - might be number[]With satisfies - type is preserved
tsconst palette = { red: '#ff0000', green: [0, 255, 0], blue: '#0000ff', } satisfies Palette; palette.red.toUpperCase(); // ok - TypeScript knows it's string palette.green.map(v => v * 2); // ok - TypeScript knows it's number[]Comparison
: Typeannotationsatisfies TypeValidates against type Yes Yes Keeps the narrower inferred type No Yes Inferred type Widened to TypeStays specific Another use case
tsconst config = { port: 3000, host: 'localhost', } satisfies Partial<ServerConfig>; // config.port is still number, not number | undefinedsatisfiesis ideal for config objects and constants where you want validation but need to keep the precise inferred type.updated
did you know it?