Skip to main content
    Courses/TypeScript/Functions and Generics

    Lesson 3 • TypeScript

    Functions 🔧

    By the end of this lesson you'll be able to type a function's parameters and return value, make arguments optional or give them defaults, accept any number of arguments, describe a function as a type, and write overloads — so TypeScript catches a wrong call before your code ever runs.

    What You'll Learn in This Lesson

    • • Type function parameters and return values (and why that stops bugs)
    • • Make a parameter optional with ? or give it a default value
    • • Use void for functions that return nothing
    • • Accept any number of arguments with rest parameters ...args
    • • Describe a function's shape with a function type expression and write arrow functions
    • • Define multiple typed call signatures with function overloads

    💡 Real-World Analogy

    A typed function is like a vending machine. The coin slot only accepts the right shape (the parameter types), and the tray always delivers the kind of item you expect (the return type). Put in the wrong coin and the machine rejects it at the slot — it doesn't take your money, jam, and break halfway through. TypeScript is that coin slot: it rejects a bad call at the door, before the machine ever runs, instead of crashing mid-purchase like plain JavaScript would.

    1. Typing Parameters & Return Values

    In TypeScript you add a type annotation after each parameter and after the parentheses for the return value. The shape is always function name(param: Type): ReturnType. This is the single most useful thing TypeScript adds to JavaScript: call the function with the wrong kind of value and you get a red squiggle in your editor, not a 2am production crash.

    // TypeScript — parameter types in green, return type after the )
    function add(a: number, b: number): number {
      return a + b;
    }
    
    function shout(text: string): string {
      return text.toUpperCase() + "!";
    }
    
    // "void" = this function returns NOTHING useful (it just does something).
    function log(message: string): void {
      console.log("[LOG]", message);
    }

    A return type of void means "this function doesn't hand a value back" — you call it for its effect (printing, saving) not its result. Run the worked example below; it's the same code with annotations removed so it executes as JavaScript.

    Worked example: parameters, returns & void

    Read every comment, run it, and check the output matches.

    Try it Yourself »
    JavaScript
    // In real TS you write the TYPES; here we run the JS so you see the result.
    // TS signature:  function add(a: number, b: number): number
    //   - a and b MUST be numbers
    //   - the function MUST return a number
    function add(a, b) {
      return a + b;
    }
    
    console.log("add(5, 3)   =", add(5, 3));      // 8
    console.log("add(10, 20) =", add(10, 20));    // 30
    
    // TS signature:  function shout(text: string): string
    function shout(text) {
      return text.toUpperCase() + "!";
    }
    console.log("shout('hi') =", sh
    ...

    2. Optional & Default Parameters

    Sometimes a parameter isn't always needed. Add ? after its name to make it optional — TypeScript then treats it as possibly undefined, so you must handle that case. Or give it a default value with = value: if the caller leaves it out, the default is used automatically. A default also makes the parameter optional, but you never have to check for undefined.

    // Optional parameter — "greeting" may be undefined, so guard it:
    function greet(name: string, greeting?: string): string {
      return (greeting ?? "Hello") + ", " + name + "!";
    }
    
    // Default parameter — cleaner: no undefined to worry about:
    function greet2(name: string, greeting: string = "Hello"): string {
      return greeting + ", " + name + "!";
    }
    // Rule: optional/default parameters must come AFTER required ones.

    Now you try. The program below is almost complete — fill in the two blanks marked ___ using the hints, then run it and check your output.

    🎯 Your turn: defaults in action

    Fill in the ___ blanks, then check your output against the expected lines.

    Try it Yourself »
    JavaScript
    // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
    // TS signature:  function greet(name: string, greeting: string = "Hello"): string
    // "greeting" has a DEFAULT, so callers can leave it out.
    
    function greet(name, greeting) {
      // 1) If greeting is missing, fall back to "Hello".
      //    (In TS the "= 'Hello'" default does this for you automatically.)
      if (greeting === undefined) greeting = ___;   // 👉 the text "Hello" in quotes
    
      // 2) Build and return the message: greeting + ",
    ...

    3. Rest Params, Arrows & Function Types

    A rest parameter (...numbers: number[]) collects "any number of" trailing arguments into a typed array — perfect when you don't know how many you'll get. An arrow function is a compact way to write a function: (n: number): number => n * 2. And a function type expression lets you describe a function's shape as a reusable type, so you can pass functions around with full type safety.

    // Rest parameter — the array type goes on the ...name:
    function sum(...numbers: number[]): number {
      return numbers.reduce((total, n) => total + n, 0);
    }
    
    // Arrow function with types:
    const double = (n: number): number => n * 2;
    
    // Function TYPE EXPRESSION — name a function shape and reuse it:
    type MathOp = (a: number, b: number) => number;
    const multiply: MathOp = (a, b) => a * b;   // a, b inferred as number
    const subtract: MathOp = (a, b) => a - b;
    
    // Now a function can accept any MathOp:
    function applyOp(a: number, b: number, op: MathOp): number {
      return op(a, b);
    }

    Read the worked example, then run it. Notice how applyOp takes a function as an argument — that's only safe because MathOp pins down exactly what that function must look like.

    Worked example: rest, arrows & function types

    See rest parameters, arrow functions, and a passed-in function type.

    Try it Yourself »
    JavaScript
    // REST PARAMETERS — collect "any number of" args into an array.
    // TS signature:  function sum(...numbers: number[]): number
    function sum(...numbers) {
      return numbers.reduce((total, n) => total + n, 0);
    }
    console.log("sum(1, 2, 3)        =", sum(1, 2, 3));        // 6
    console.log("sum(10, 20, 30, 40) =", sum(10, 20, 30, 40)); // 100
    console.log("sum()               =", sum());               // 0
    
    // ARROW FUNCTIONS — a shorter way to write a function.
    // TS signature:  const double = (n: numb
    ...

    Your turn again — finish the average function. It uses a rest parameter and an arrow inside reduce. Fill in the two blanks:

    🎯 Your turn: average with a rest param

    Fill in the starting total and the final calculation, then run it.

    Try it Yourself »
    JavaScript
    // 🎯 YOUR TURN — fill in the blanks to finish "average".
    // TS signature:  function average(...scores: number[]): number
    
    function average(...scores) {
      // 1) Add up every score. Use reduce with an arrow function.
      const total = scores.reduce((sum, n) => sum + n, ___);  // 👉 the starting total, 0
    
      // 2) Guard against divide-by-zero, then return the mean.
      if (scores.length === 0) return 0;
      return ___;                                              // 👉 total / scores.length
    }
    
    console.lo
    ...

    4. Function Overloads

    An overload lets one function advertise several different typed call signatures. You write the signatures first (no body), then one implementation that handles them all. Callers only ever see the clean, specific signatures — TypeScript hides the messy string | number implementation underneath. This is how you say "this function behaves differently depending on what you give it" while keeping every call fully type-checked.

    // Two overload signatures (no body) — the public contract:
    function format(value: string): string;
    function format(value: number): string;
    // One implementation signature (must be compatible with both):
    function format(value: string | number): string {
      if (typeof value === "string") {
        return value.trim().toUpperCase();
      }
      return value.toFixed(2);
    }
    
    format("hi");     // OK → string
    format(3.14159);  // OK → string
    // format(true);  // ❌ Error: no overload matches a boolean

    Run the worked version below. The behaviour branches on typeof value at runtime, while the overload signatures give callers precise, safe types at compile time.

    Worked example: function overloads

    One function, two typed signatures — string and number both return a string.

    Try it Yourself »
    JavaScript
    // FUNCTION OVERLOADS — one function, several typed call signatures.
    // TS:
    //   function format(value: string): string;     // overload 1
    //   function format(value: number): string;     // overload 2
    //   function format(value: string | number): string {  // implementation
    //     ...
    //   }
    // Callers see ONLY the two clean signatures, not the messy implementation.
    
    function format(value) {
      if (typeof value === "string") {
        return value.trim().toUpperCase();    // string branch
      }
      retu
    ...

    🔎 Deep Dive: arrow functions vs function

    Arrow functions aren't just "shorter function". The key difference is this: an arrow function does not create its own this — it borrows the one from where it was written. That makes arrows the right choice for callbacks (like inside map, filter, reduce, or event handlers) where you want to keep the surrounding this.

    For a one-line return you can drop the braces and the word return: (n) => n * 2. If you need braces for multiple statements, you must write return yourself.

    const double = (n: number): number => n * 2;        // implicit return
    const greet  = (name: string): string => {           // braces → explicit return
      const msg = "Hi " + name;
      return msg;
    };

    Putting It Together: a Pricing Function

    This small function uses everything from the lesson at once: a required parameter, a default parameter, a rest parameter, and an explicit string return type. Read it line by line — you understand every part now.

    Worked example: flexible price()

    Required base, default tax rate, and any number of extra fees.

    Try it Yourself »
    JavaScript
    // === A tiny pricing function — everything from this lesson together ===
    // TS signature:
    //   function price(base: number, taxRate: number = 0.2, ...fees: number[]): string
    //     - base:    required number
    //     - taxRate: DEFAULT 0.2 (20%) if not given
    //     - fees:    REST — zero or more extra charges
    //     - returns: a string
    
    function price(base, taxRate = 0.2, ...fees) {
      const tax = base * taxRate;                       // tax on the base
      const extra = fees.reduce((t, f) => t + f,
    ...

    Order matters: the required base comes first, then the taxRate with its default, then the ...fees rest parameter — which must always be last.

    Pro Tips

    • 💡 Always annotate parameters. Return types can often be inferred, but typing parameters is what catches wrong calls.
    • 💡 Prefer a default over an optional when there's a sensible fallback — it removes the undefined check entirely.
    • 💡 Put required params first. Optional, default, and rest parameters must come after the required ones.
    • 💡 Name a function type with type Handler = (e: Event) => void; and reuse it instead of repeating the signature.

    Common Errors (and the fix)

    • "TS7006: Parameter 'x' implicitly has an 'any' type": you forgot to type a parameter. Add an annotation — function f(x: number) — or enable it knowingly. (This fires under noImplicitAny, which is on in strict mode.)
    • "TS2554: Expected 2 arguments, but got 1" (or 3): you called the function with the wrong number of arguments. Make a parameter optional with ? or give it a default, or fix the call site.
    • Missing return type bites you: if you forget to return in a function annotated : number, TS reports "TS2355: A function whose declared type is neither 'void' nor 'any' must return a value". Add the return, or change the type to void.
    • "TS2322: Type 'string' is not assignable to type 'number'": you returned the wrong type. Return value must match the declared return type.
    • "A required parameter cannot follow an optional parameter": reorder so required parameters come first.

    📋 Quick Reference

    FeatureSyntax
    Typed functionfunction f(a: number): string
    Returns nothingfunction log(m: string): void
    Optional paramfunction f(name?: string)
    Default paramfunction f(n: number = 0)
    Rest paramfunction f(...xs: number[])
    Arrow functionconst d = (n: number): number => n * 2
    Function typetype Op = (a: number, b: number) => number
    Overloadfunction f(x: string): string;

    Frequently Asked Questions

    Q: What's the difference between ? and = default?

    Both let the caller skip the argument. With ? the parameter can be undefined, so you must handle that. With = value it falls back to that value automatically, so it's never undefined inside the function.

    Q: Do I always have to write the return type?

    No — TypeScript usually infers it from your return statements. But writing it is good practice: it documents intent and turns a wrong return into an error at the function, not at some far-away call site.

    Q: When should I use overloads instead of a union type?

    Use a union (string | number) when the return type is the same no matter the input. Use overloads when the relationship between input and output changes — e.g. a string input returns a string and a number input returns a number array.

    Q: Is an arrow function the same as a regular function?

    Almost — the big difference is this. Arrow functions don't bind their own this; they reuse the one from where they're defined, which makes them ideal for callbacks. For most plain helper functions, either style works.

    Mini-Challenge: joinNames

    No blanks this time — just a brief and an outline. Write the body yourself using a default parameter and a rest parameter, run it, and check your output against the expected lines.

    🎯 Mini-Challenge: build joinNames

    Combine a default separator and a rest parameter into one function.

    Try it Yourself »
    JavaScript
    // 🎯 MINI-CHALLENGE: a flexible "joinNames" function
    // TS signature to aim for:
    //   function joinNames(separator: string = ", ", ...names: string[]): string
    //
    // 1. Give "separator" a DEFAULT of ", " (comma + space).
    // 2. Use a REST parameter "...names" to accept any number of names.
    // 3. Return the names glued together with the separator.
    //    (hint: an array has a .join(separator) method)
    // 4. If no names are passed, return "(nobody)".
    //
    // ✅ Expected:
    //    joinNames(", ", "Ann", "Be
    ...

    🎉 Lesson Complete

    • ✅ Annotate parameters and return types: function f(a: number): string
    • void marks a function that returns nothing useful
    • ? makes a parameter optional; = value gives it a default
    • ...args: T[] collects any number of arguments into a typed array
    • ✅ Arrow functions and function type expressions let you pass functions around safely
    • ✅ Overloads expose several clean signatures over one implementation
    • Next lesson: Classes and Objects — bundle data and behaviour together

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service