Ace your TypeScript interview with 50+ questions covering type system, generics, utility types, and best practices.
Both define object shapes, but: interfaces support declaration merging (can be extended by declaring again), types support unions and intersections more naturally. Interfaces are preferred for public APIs and when you might extend. Types are preferred for complex type compositions, unions, and primitives. Performance is essentially identical.
Utility types are built-in type transformations: Partial<T> makes all properties optional, Required<T> makes all required, Readonly<T> makes all readonly, Pick<T, K> selects subset of properties, Omit<T, K> removes properties, Record<K, V> creates object type. They reduce boilerplate and enable type manipulation.
void represents absence of return value (function returns nothing). never represents values that never occur (function throws or infinite loop). unknown is the type-safe counterpart to any - you must narrow the type before using it. Use void for function return types, never for exhaustive checks, unknown for values of unknown type.
Type narrowing refines a type to a more specific type within a code block. Type guards are expressions that perform runtime checks and narrow types: typeof (primitives), instanceof (classes), in (property check), custom type predicates (function returning "arg is Type"). TypeScript uses control flow analysis to track narrowed types.
Declaration merging combines multiple declarations with the same name into one. Interfaces can be merged (properties combined), namespaces can be merged, classes merge with namespaces. Use for: extending third-party types, module augmentation, adding properties to global scope. Types (type aliases) cannot be merged.
any disables type checking - you can do anything with it (unsafe). unknown requires type checking before use (type-safe). Use unknown when you receive data of unknown type (API responses, user input) and validate before use. Avoid any; it defeats TypeScript's purpose. unknown forces explicit type narrowing.
TypeScript uses structural typing (duck typing) — types are compatible if they have the same shape, regardless of name. Nominal typing (Java, C#) requires explicit declaration of type relationships. Implication: {name: string} is assignable to any type that needs {name: string}. You can simulate nominal typing using branded types (type UserId = string & {_brand: "UserId"}).
Discriminated unions use a common literal property (discriminant) to distinguish union members. Example: type Shape = {kind: "circle", radius: number} | {kind: "square", side: number}. TypeScript narrows the type in switch/if based on the discriminant. Use for: state machines, action types in reducers, API response variants. Always add an exhaustive check with never.
Type assertion (as Type or <Type>) tells TypeScript to treat a value as a specific type — no runtime check, you take responsibility. Type guard performs an actual runtime check and narrows the type in that branch. Use assertions only when you know TypeScript is wrong. Prefer type guards for safety. Double assertion (as unknown as Type) should be a red flag.
Pick<T, K>: creates type with only the specified keys. Omit<T, K>: creates type without specified keys. Exclude<T, U>: removes union members assignable to U. Extract<T, U>: keeps only union members assignable to U. NonNullable<T>: removes null/undefined. Parameters<T>: extracts function parameter types as tuple. ReturnType<T>: extracts return type.
Generics allow creating reusable components that work with multiple types while maintaining type safety. Example: function identity<T>(arg: T): T { return arg; }. The type is determined when called: identity<string>("hello"). Use generics for: reusable functions, container types (arrays, promises), when relationships between input/output types matter.
Generic constraints (extends) restrict what types can be passed. Example: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]. Multiple constraints: T extends A & B. Default types: <T = string>. Conditional generics with infer enable type-level programming. Avoid over-constraining — use the minimum constraint needed for the implementation.
Mapped types transform properties: { [K in keyof T]: newType }. Conditional types choose types based on conditions: T extends U ? X : Y. Combined, they enable powerful type transformations. Example: making all properties optional, filtering properties by type, extracting return types. They're the foundation of utility types.
infer declares a type variable within conditional types, extracting types from other types. Example: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never extracts the return type. Used for: extracting array element types, promise resolved types, function parameter types. It enables complex type inference.
Module augmentation extends existing modules by re-declaring them. Useful for: adding methods to existing types (Express Request), fixing missing type declarations, extending third-party library types. Use declare module "module-name" {} syntax. For global augmentation, use declare global {}. Common for adding req.user to Express or extending window object.
Template literal types compose string literal types: type Greeting = `Hello, ${string}`. Combine with unions for exhaustive string type generation: type EventName = `on${Capitalize<"click"|"focus">}`. Built-in: Uppercase, Lowercase, Capitalize, Uncapitalize. Use for: typed CSS properties, event name generation, API endpoint typing, enforcing naming conventions.
For function components: React.FC<Props> or (props: Props) => JSX.Element. Define props interface, use children: React.ReactNode, events like React.MouseEvent<HTMLButtonElement>. For hooks: useState<Type>(), useRef<HTMLDivElement>(null). Generic components: function List<T>(props: { items: T[] }). Avoid using any; prefer strict typing.
strict: true enables: strictNullChecks (null/undefined not assignable to other types), noImplicitAny (error on implicit any), strictFunctionTypes (function parameter contravariance), strictPropertyInitialization (class properties must be initialized), strictBindCallApply, noImplicitThis. Enable strict mode for new projects. Each option can be enabled individually for gradual migration.
Declaration files describe the shape of existing JavaScript to TypeScript. Auto-generated with tsc --declaration. For libraries without TypeScript: install @types/package-name (DefinitelyTyped). Write your own when no types exist. Key: declare module, declare global, export. tsconfig.json types/typeRoots controls which @types packages are included.
Practice with interactive quizzes and get instant feedback.
Start Free Practice