Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Objects & Interfaces
Objects are the core of JavaScript, and TypeScript provides powerful ways to describe their shapes. Interfaces and type aliases are your primary tools for defining object structures. Think of an interface as a blueprint or contract — it does not create anything at runtime, but it defines the exact shape that any conforming object must follow. If an object claims to be aUser, the interface guarantees it has an id, a name, and an email — no more guessing.
1. Interfaces
Interfaces define contracts for object shapes. They are the most common way to type objects in TypeScript, and they are one of the features teams adopt first because the payoff is immediate — autocomplete, refactoring safety, and self-documenting code.Basic Interface
Optional Properties
Readonly Properties
2. Type Aliases vs Interfaces
Both can describe object shapes, and for everyday object typing, they are interchangeable. The “interface vs type” debate is one of the most common questions in TypeScript, and the honest answer is: it matters less than people think. That said, there are real differences that matter in specific situations.Syntax Differences
Key Differences
3. Extending Interfaces
Build complex types by extending existing ones.Single Inheritance
Multiple Inheritance
Extending Type Aliases
4. Intersection Types
Combine multiple types into one using the& operator. If interfaces are like inheriting from a parent, intersections are like mixing ingredients — you take everything from Type A and everything from Type B and combine them into a single type that has all properties from both.
Intersection with Conflicts
5. Index Signatures
Define types for objects with dynamic keys. Index signatures are essential when you do not know all the property names ahead of time — like a dictionary, a cache, or a configuration object where users can add arbitrary keys.String Index Signature
Number Index Signature
Mixing Index Signatures with Properties
6. Mapped Types
Transform existing types into new ones using afor...in-style syntax at the type level. Mapped types are one of TypeScript’s most powerful features — they let you derive new types from existing ones programmatically, rather than manually rewriting every property. Think of them as a “type factory” that takes one type as input and produces a modified version as output.
Basic Mapped Type
Built-in Utility Types
TypeScript provides several utility types based on mapped types:7. Template Literal Types
Create types from string patterns. Template literal types are one of TypeScript’s most unique features — no other mainstream language can express “a type that is any string matching this pattern.” They let you bring the same compile-time safety to strings that you have for objects and numbers.Practical Example: CSS Properties
8. Recursive Types
Types that reference themselves. Recursive types model data structures that are inherently self-similar — trees, nested comments, JSON values, or file system directories. They are TypeScript’s answer to the question “how do I type something that can contain itself?“9. Interface for Functions and Arrays
Interfaces can describe more than plain objects.Function Interface
Array Interface
Hybrid Types
10. Practical Patterns
Builder Pattern
Repository Pattern
Summary
| Concept | Example |
|---|---|
| Interface | interface User { name: string } |
| Optional Property | { name?: string } |
| Readonly Property | { readonly id: number } |
| Extending | interface Dog extends Animal |
| Intersection | type Staff = Person & Employee |
| Index Signature | { [key: string]: number } |
| Mapped Types | { [K in keyof T]: T[K] } |
| Template Literals | type Event = `on${string}` |
| Utility Types | Partial<T>, Pick<T, K>, Omit<T, K> |
Interview Deep-Dive
Q: When would you use an interface versus a type alias for defining an object shape? Is the community advice of 'always use interfaces' correct?
Q: When would you use an interface versus a type alias for defining an object shape? Is the community advice of 'always use interfaces' correct?
- Interfaces are better when: You are defining an object shape that will be extended or implemented. Interfaces support declaration merging — you can declare the same interface name twice and TypeScript combines them. This is essential for augmenting third-party types:
declare module 'express' { interface Request { user?: User } }works because of declaration merging. You cannot do this with type aliases. Interfaces also produce better error messages because they have names that appear in diagnostics. - Type aliases are better when: You need unions, intersections, tuples, or computed types.
type Result = Success | Failurerequires a type alias. Mapped types, conditional types, and template literal types all require type aliases. An interface cannot represent a union of two shapes. - They are equivalent when: You are defining a simple object shape that will not be extended or merged. Pick one style for consistency and move on.
- My approach: Interfaces for entities and contracts (things that classes implement or that represent domain objects), type aliases for everything else (unions, computed types, utility types, function signatures).
interface Window { myCustomProp: string } and it adds myCustomProp to the existing Window interface rather than replacing it. This is powerful for extending global types and third-party libraries. The footgun is accidental merging: if two files independently declare interface Config { ... } with different properties, TypeScript silently merges them. You end up with a Config that has properties from both files, which may not be your intent. Type aliases prevent this because redeclaring a type alias is a compile error. This is one argument for preferring type aliases in application code and reserving interfaces for library type augmentation.Q: Explain index signatures. When are they useful, and what is the biggest pitfall that catches experienced developers?
Q: Explain index signatures. When are they useful, and what is the biggest pitfall that catches experienced developers?
- When they are useful: Configuration objects, dictionaries, caches, and any key-value mapping where the keys come from external data. Think of a translation file:
{ en: 'Hello', fr: 'Bonjour', de: 'Hallo' }— you do not know all the locale codes at compile time, but you know every value is a string. - The biggest pitfall: false confidence in property access. When you access
dict['someKey'], TypeScript returns the value type (e.g.,number) withoutundefined, even though the key might not exist. This is TypeScript lying to you for convenience. If you enablenoUncheckedIndexedAccessintsconfig.json, TypeScript correctly returnsnumber | undefinedfor index signature access, forcing you to handle the missing-key case. Without this flag,const value: number = dict['nonexistent']compiles without error but returnsundefinedat runtime. - The compatibility constraint: All explicitly declared properties must have types compatible with the index signature. If you write
interface User { id: number; [key: string]: string }, it fails becauseid: numberis not compatible with the index signature’s value typestring. - Alternative: Record and Map.
Record<string, number>is syntactic sugar for an index signature. For runtime dynamic keys,Map<string, number>is often better because it does not conflate your data keys with object prototype properties and has proper.has()and.get()methods.
noUncheckedIndexedAccess do, and should every project enable it?It makes TypeScript add | undefined to the return type of every index signature access and array index access. This is technically correct — arrays can be empty — but it requires nullish checks on every array access. My recommendation: enable it for new projects handling external data. For existing projects, the migration cost may not justify the safety gain.Q: Walk me through how intersection types work with conflicting properties. What happens when you intersect two types that have the same property name with different types?
Q: Walk me through how intersection types work with conflicting properties. What happens when you intersect two types that have the same property name with different types?
- Compatible properties: merged normally.
{ name: string } & { age: number }produces{ name: string; age: number }. - Identical properties: collapsed.
{ name: string } & { name: string }produces{ name: string }. - Conflicting properties: resolved to
never.{ value: string } & { value: number }produces{ value: string & number }, which is{ value: never }. There is no value that is both astringand anumber, so the intersection isnever. You can declare a variable of this type, but you can never create a valid value for it. - Why this matters in practice: It comes up when composing types from different sources. If a library exports
type WithId = { id: string }and your codebase hastype Entity = { id: number }, intersecting them produces{ id: never }— silently unusable. The fix is ensuring compatible types before intersecting, or usingOmitto remove conflicting properties first. - Method signatures are different: If both types have a method with the same name but different signatures, the intersection creates an overloaded method. This is intentional and useful for composing interfaces with polymorphic methods.
{ value: string } | { value: number }, accessing value gives you string | number — because the value could come from either variant. You must narrow before using variant-specific methods. The key insight: intersection types combine the properties (you have ALL of them), while union types restrict to common properties (you can only access what is guaranteed across ALL variants).Q: You are building a repository layer with TypeScript generics. How would you design the type signatures for CRUD operations to be both type-safe and practical?
Q: You are building a repository layer with TypeScript generics. How would you design the type signatures for CRUD operations to be both type-safe and practical?
- Base interface with two type parameters:
interface Repository<T, ID>whereTis the entity type andIDis the identifier type. Two parameters because not every entity uses the same ID type. - Create signature using
Omit:create(entity: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>. The caller should not provide auto-generated fields.Omitremoves them from the input type while the return type is the full entity. - Update signature using
Partial:update(id: ID, entity: Partial<T>): Promise<T>. Partial updates are the norm — you rarely send every field. - FindById returns nullable:
findById(id: ID): Promise<T | null>. NotT | undefined— null is the conventional “not found” signal in database contexts. - Constrain
Twith a base entity:T extends { id: ID }ensures every entity has anidfield. This lets the repository implementation rely onentity.idwithout asserting.
Omit, Partial, Pick) derive these from a single source-of-truth type, so you maintain one definition and get all the variants automatically.Follow-up: How would you make this repository pattern testable? Would you use interfaces or abstract classes?I would define the repository as an interface, not an abstract class. Interfaces have no runtime presence — they are pure contracts. For testing, I create a mock implementation that stores data in a plain array without pulling in database dependencies. For production, a real implementation uses the database. The consuming code depends on the interface, not the implementation, following dependency inversion. Abstract classes would work but are heavier — they carry implementation code and cannot be multiply inherited. For a pure data-access contract, interfaces are the right tool.