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.
Functions & Types
Functions are the building blocks of any application. TypeScript makes functions safer by typing parameters, return values, and function signatures themselves. Think of typed function parameters as a form with labeled fields — instead of accepting “anything” and hoping the caller passes the right data, you specify exactly what each slot expects. The compiler then rejects any call that does not match the form.1. Function Type Annotations
Parameter and Return Types
void and never
never type is especially useful in exhaustive switch statements. If you handle all cases, the default branch’s variable becomes never — and if you add a new case later without handling it, the compiler errors. This is a powerful safety net for growing codebases.
2. Optional and Default Parameters
Optional Parameters
Default Parameters
Rest Parameters
3. Function Type Expressions
Define the type of a function itself — not what it does, but what its shape looks like. This is like defining a “job description” that any function matching the signature can fill.Call Signatures
Construct Signatures
4. Function Overloads
Define multiple function signatures for different parameter combinations. Overloads let you express something that a single signature cannot: “if you pass a string, you get back X; if you pass a number, you get back Y.” The compiler uses the overload signatures (the declarations) to check your call sites, while the implementation signature (the body) handles the actual logic.More Complex Overloads
5. Generic Functions
Write functions that work with any type while maintaining type safety. Generics are like templates or molds — you define the shape once, and the caller fills in the specific type. Without generics, you would have to choose between type safety (writing separate functions for each type) and flexibility (usingany and losing all type information).
Basic Generics
Multiple Type Parameters
Generic Constraints
keyof Constraint
6. Type Guards
Functions that narrow types at runtime. Type guards bridge the gap between TypeScript’s compile-time type system and JavaScript’s runtime behavior. They let you write conditional logic that both the runtime and the compiler understand.typeof Guards
instanceof Guards
Custom Type Guards (Type Predicates)
Custom type guards are one of the most powerful patterns in TypeScript. Theis keyword in the return type tells the compiler: “if this function returns true, treat the parameter as the specified type.” This lets you encapsulate complex runtime checks in a reusable function.
isUser(data: unknown): data is User function that validates the shape of external data, giving you both runtime safety and compile-time narrowing in one step.
Discriminated Unions
7. Assertion Functions
Functions that throw if a condition is false, narrowing the type for all code that follows. Unlike type predicates (which narrow inside anif block), assertion functions narrow the type for the rest of the function scope — everything below the assertion call benefits from the narrowed type.
Non-null Assertion
8. this Parameter
Explicitly type thethis context.
This Parameter in Classes
9. Callback Types
Type callbacks precisely.10. Practical Examples
API Function Types
Event Emitter Pattern
Summary
| Concept | Example |
|---|---|
| Parameter Types | function add(a: number, b: number) |
| Return Types | function add(a, b): number |
| Optional Params | function greet(name: string, greeting?: string) |
| Default Params | function greet(name: string, greeting = 'Hello') |
| Rest Params | function sum(...nums: number[]) |
| Function Types | type Fn = (x: number) => number |
| Overloads | Multiple signatures + implementation |
| Generics | function identity<T>(value: T): T |
| Type Guards | function isFish(x): x is Fish |
| Assertions | function assert(x): asserts x is string |
Interview Deep-Dive
Q: When would you use function overloads versus a union return type? What trade-offs are you making?
Q: When would you use function overloads versus a union return type? What trade-offs are you making?
Strong Answer:This is a design decision that many TypeScript developers get wrong because they default to overloads when simpler alternatives exist.
- Use overloads when: The return type depends on the input type, and you want the caller to get the precise return type based on what they passed. Classic example:
createElement('div')returnsHTMLDivElement,createElement('input')returnsHTMLInputElement. A single signaturecreateElement(tag: string): HTMLElementloses this precision — the caller always gets the genericHTMLElementand must cast. Overloads preserve the mapping from specific input to specific output. - Use union return types when: The function legitimately returns different types regardless of input, or the relationship between input and output is not 1-to-1. For example,
function parse(input: string): number | Error— every input is a string, and the output depends on the content, not the type. Overloads would not help here because there is no type-level distinction between “a string that parses” and “a string that does not.” - The trade-off: Overloads add complexity. The implementation signature must be compatible with all overload signatures, which often means the implementation body is full of type assertions and conditional logic. If you have 5 overloads, you have 5 signatures to maintain. Generics with conditional types often achieve the same result with less code:
function identity<T extends string | number>(value: T): T extends string ? string : numbergives you input-dependent return types without overloads. - My rule of thumb: If you have 2-3 distinct input-output mappings, overloads are clear and readable. If you have more, generics with conditional types are usually cleaner. If the return type does not depend on the input type, just use a union.
string | number | Date, but you only have overloads for string and number, callers cannot pass a Date even though the implementation handles it. The overload signatures act as the public API; the implementation signature is an internal detail. A common mistake is writing an implementation signature that is too narrow, causing one of the overloads to be incompatible, which TypeScript flags as an error.Q: Explain assertion functions (asserts keyword) versus type predicates (is keyword). When do you reach for each?
Q: Explain assertion functions (asserts keyword) versus type predicates (is keyword). When do you reach for each?
Strong Answer:Both are tools for narrowing types, but they work in fundamentally different scopes and have different failure modes.
- Type predicates (
is): Used inifconditions.function isString(x: unknown): x is stringnarrows the type inside theifblock where the return value istrue. The function returns a boolean — it does not throw. The caller decides what to do when the check fails (return early, show an error, use a default). Narrowing is limited to theifblock. - Assertion functions (
asserts): Narrow the type for the entire remaining scope.function assertIsString(x: unknown): asserts x is stringthrows if the condition fails. After the call, TypeScript treatsxasstringfor every line that follows — not just inside anifblock. The function either succeeds (and narrows) or throws (and execution stops). There is no “else” path. - When to use type predicates: Filtering, branching, optional handling.
const users = items.filter(isUser)uses a type predicate to narrow the array type.if (isAdmin(user)) { showAdminPanel(); }— the non-admin path is a valid execution path, not an error. - When to use assertion functions: Validation at boundaries where invalid data is a bug, not an expected condition.
assertDefined(config.apiKey)at application startup — if the API key is missing, crashing immediately with a clear error message is the correct behavior. Assertion functions are also useful in test setup:assertIsHTMLElement(element)before interacting with it. - The danger of assertion functions: If your assertion is wrong (the function returns without throwing when it should have thrown), TypeScript trusts you and narrows the type incorrectly. This is a silent type safety violation. Type predicates are safer because they return a boolean that can be unit-tested; assertion functions are harder to test for the “should have thrown but did not” case.
Array.prototype.filter expects a function returning boolean, and assertion functions return void (they assert or throw). You must use type predicates with filter: items.filter((item): item is User => isUser(item)). Assertion functions are for imperative code paths where you want to “gate” execution, not for declarative operations like filtering or mapping.Q: What are generic constraints in functions, and what problem does 'keyof' solve that a plain generic cannot?
Q: What are generic constraints in functions, and what problem does 'keyof' solve that a plain generic cannot?
Strong Answer:Generic constraints are how you tell TypeScript “this type parameter is not completely open — it must satisfy certain requirements.” Without constraints, generics are too permissive; with them, you get precision.
- The problem without constraints:
function getLength<T>(value: T): number { return value.length; }fails because TypeScript does not know thatThas alengthproperty.Tcould benumber, which has nolength. The compiler correctly rejects this. - The solution with
extends:function getLength<T extends { length: number }>(value: T): numbertells TypeScript: “T can be any type, as long as it has alengthproperty of typenumber.” Now strings, arrays, and any object withlengthwork, but numbers are rejected at the call site. - Where
keyofelevates this:function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]constrainsKto be one of the actual keys ofT. IfTis{ name: string, age: number }, thenKcan only be'name'or'age'. The return typeT[K]is automatically the type of that specific property — if you pass'name', you getstring; if you pass'age', you getnumber. Withoutkeyof, you would have to acceptkey: stringand returnany, losing all type safety. - Real-world example: A type-safe
pickfunction:function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>. This guarantees at compile time that you can only pick keys that exist on the object, and the return type precisely reflects which keys you picked. This is impossible without thekeyofconstraint.
T extends keyof U and K extends keyof T in terms of how the types are resolved?K extends keyof T constrains K to be a union of string literal types representing the keys of T. When you call getProperty(user, 'name'), TypeScript infers T = typeof user and K = 'name'. The return type T[K] resolves to T['name'] which is string. If T is generic (not yet resolved), T[K] remains a deferred type that resolves when T is known. The key insight is that keyof produces a union of literal types at the type level, not a runtime value — it is purely a compile-time constraint that prevents invalid property access.Q: How does TypeScript handle the 'this' parameter in functions, and why does it matter for event handlers and class methods?
Q: How does TypeScript handle the 'this' parameter in functions, and why does it matter for event handlers and class methods?
Strong Answer:The
this parameter is TypeScript’s solution to one of JavaScript’s most notorious footguns: the value of this depends on how a function is called, not how it is defined.- The problem: In JavaScript, if you extract a method from an object and call it standalone,
thisisundefined(strict mode) or the global object (sloppy mode).const greet = user.greet; greet();—this.nameisundefinedinsidegreetbecause the method lost its binding. This is the root cause of countless bugs in event handlers:button.addEventListener('click', user.handleClick)— insidehandleClick,thisis the button element, not the user object. - TypeScript’s solution: The
thisparameter is a fake first parameter that is erased at compile time (like type annotations).greet(this: User): stringtells TypeScript: “this function must be called withthisbound to aUser.” If you try to call it without the right context, TypeScript errors. This catches the detached method bug at compile time. - In classes, arrow functions are the pragmatic fix:
increment = () => { this.count++; }capturesthislexically from the class instance. It is safe to pass as a callback becausethisalways refers to the instance. The trade-off is that arrow methods are per-instance (each instance gets its own copy), not on the prototype. For a class with 10,000 instances, that is 10,000 copies of the function instead of one shared prototype method. For UI components (where instances are few), this is irrelevant. For data classes in tight loops, it can matter. strictBindCallApplyconfig: When enabled, TypeScript checks thatFunction.prototype.bind,.call, and.applyare called with the correct types.greet.call({ name: 42 })would error because{ name: 42 }does not match thethis: Userparameter. Without this flag, these calls are unchecked.
this parameter over an arrow function in a class?When you want the method on the prototype (shared across instances) and you control all call sites. For a library API where you document “call this method on the object, do not extract it,” the this parameter provides compile-time enforcement without the per-instance memory cost. Arrow functions are better when the method will be passed as a callback (event handlers, React callbacks, Promise chains) because you cannot control how the caller invokes it. The judgment call is: “will this method ever be detached from its object?” If yes, arrow function. If no, prototype method with this parameter.