Lab 02: Functions & Type Signatures

Objective

Write typed functions with parameters, return types, overloads, generics, and higher-order functions. Understand arrow functions, optional/rest parameters, and function type signatures.

Background

TypeScript's function types are its most powerful feature for eliminating bugs. Typed parameters catch wrong argument orders, optional parameters document intent, and generic functions work across types without sacrificing safety. TypeScript also supports function overloads — multiple signatures for one implementation.

Time

25 minutes

Prerequisites

  • Lab 01 (Hello World)

Tools

  • Docker image: zchencow/innozverse-ts:latest


Lab Instructions

Step 1: Function Types & Signatures

💡 Function types are just type annotations for functions: (a: number, b: number) => number describes any function that takes two numbers and returns a number. TypeScript uses structural typing — any function with this shape is assignable, regardless of what it's called.

📸 Verified Output:


Step 2: Optional, Default & Rest Parameters

💡 Optional vs default: param?: string means the caller may omit it (receives undefined). param: string = "default" means the caller may omit it (receives "default"). Prefer defaults over optional when a sensible default exists — it makes the function easier to call and the code cleaner.

📸 Verified Output:


Step 3: Generics

💡 Generics let you write once, use for any type. Without generics, you'd need numberFirst, stringFirst, userFirst, etc. With first<T>, one function works for all types while preserving type information. The type parameter T is inferred from the argument — you rarely need to write first<number>(arr) explicitly.

📸 Verified Output:


Step 4: Function Overloads

💡 The implementation signature is not visible to callers — only the overload signatures are. The implementation signature must be compatible with all overloads (usually using union types). Keep overloads minimal — if the function has wildly different behavior per type, consider separate functions.

📸 Verified Output:


Step 5: Higher-Order Functions

💡 TypeScript's generic pipe preserves types through transformations. If the functions have different input/output types, you need a more complex variadic generic type (like fp-ts's pipe). For same-type pipelines (string → string), this pattern is clean and fully typed.

📸 Verified Output:


Step 6: Async Functions

💡 async functions always return Promise<T> — even if you write return 42, TypeScript infers Promise<number>. The await keyword unwraps the promise to get T. TypeScript tracks this automatically, so const x = await fetchData(1) has type { id: number; title: string }, not Promise<...>.

📸 Verified Output:


Step 7: Type Guards

💡 value is Type return type makes a function a type guard — after if (isCat(animal)), TypeScript narrows animal's type to Cat in the if-branch and Dog in the else-branch. This is how TypeScript achieves safe type narrowing from union types without casting (as).

📸 Verified Output:


Step 8: Complete — Typed Pipeline

💡 The Result type makes errors explicit in the type system — callers must handle both ok and error cases. Compared to exceptions, Result types are visible in function signatures (Result<number> vs number) and force callers to handle errors at the call site. TypeScript's discriminated unions make this pattern ergonomic.

📸 Verified Output:


Summary

TypeScript's function system is expressive and type-safe. You've covered typed parameters, optional/default/rest args, generics, overloads, higher-order functions, async/await types, type guards, and the Result pattern. These skills make every function self-documenting and IDE-friendly.

Further Reading

Last updated