🎯 TypeScript Function Types: Your Recipe Book for Cooking Up Code!
Imagine you’re running a magical kitchen. Every recipe (function) needs instructions about what ingredients go in and what delicious dish comes out. TypeScript function types are like recipe cards that tell everyone exactly what your kitchen can make!
🍳 Function Type Expressions: Writing Your Recipe Cards
A function type expression is like writing on a recipe card: “This recipe takes flour and sugar → gives you a cake.”
// The recipe card format:
// (ingredients) => result
type Greeter = (name: string) => string;
const sayHello: Greeter = (name) => {
return `Hello, ${name}!`;
};
sayHello("Alice"); // "Hello, Alice!"
How to Read It
(name: string) => string
↓ ↓
input output
It’s that simple! Left side = what goes in. Right side = what comes out.
Real Examples
// A recipe that adds two numbers
type Adder = (a: number, b: number) => number;
// A recipe that checks if someone is old enough
type AgeChecker = (age: number) => boolean;
// A recipe that takes nothing, returns nothing
type Logger = () => void;
📞 Call Signatures: Fancy Recipe Cards with Extra Details
Sometimes your recipe card needs more space to explain things. Call signatures let you describe functions inside an object type.
type Calculator = {
(a: number, b: number): number;
description: string;
};
const add: Calculator = (a, b) => a + b;
add.description = "Adds two numbers";
add(5, 3); // 8
add.description; // "Adds two numbers"
When to Use Call Signatures
Use them when your function also needs to carry extra information, like a recipe card that has notes on the back!
graph TD A[Function Type Expression] --> B["#40;a, b#41; => number"] C[Call Signature] --> D["{#40;a, b#41;: number}"] D --> E[Can add properties!]
🏗️ Construct Signatures: Building New Things
Some functions don’t just return a value—they build something new using the new keyword. Like a factory that builds toys!
class Robot {
name: string;
constructor(name: string) {
this.name = name;
}
}
type RobotFactory = {
new (name: string): Robot;
};
function buildRobot(factory: RobotFactory) {
return new factory("Beep-Boop");
}
const myRobot = buildRobot(Robot);
// myRobot.name === "Beep-Boop"
The Magic Word: new
// Regular call signature (just calls)
type Caller = {
(x: number): string;
};
// Construct signature (builds with 'new')
type Constructor = {
new (x: number): SomeClass;
};
Think of it like:
- Call signature = “Ask the chef to cook”
- Construct signature = “Ask the factory to build a new chef”
❓ Optional Parameters: Maybe You Want Extra Toppings?
Sometimes pizza comes with pepperoni, sometimes without. Optional parameters work the same way—they’re there if you want them!
Add a ? after the parameter name to make it optional.
function greet(name: string, title?: string) {
if (title) {
return `Hello, ${title} ${name}!`;
}
return `Hello, ${name}!`;
}
greet("Alice"); // "Hello, Alice!"
greet("Alice", "Dr."); // "Hello, Dr. Alice!"
Important Rules
- Optional parameters must come after required ones
- Optional parameters become
type | undefined
// ✅ Good: optional after required
function good(a: string, b?: number) {}
// ❌ Bad: optional before required
function bad(a?: string, b: number) {}
// Error! Required after optional
🎁 Default Parameters: Pre-Set Favorites
What if most people want cheese on their burger? Set a default value so they don’t have to ask every time!
function orderBurger(
size: string = "medium",
cheese: boolean = true
) {
return `${size} burger, cheese: ${cheese}`;
}
orderBurger(); // "medium burger, cheese: true"
orderBurger("large"); // "large burger, cheese: true"
orderBurger("small", false);// "small burger, cheese: false"
Defaults vs Optionals
graph TD A[Optional: ?] --> B[Value might be undefined] C[Default: = value] --> D[Always has a value] D --> E[Uses default if not provided]
// Optional: might be undefined
function opt(x?: number) {
// x could be undefined!
}
// Default: always has value
function def(x: number = 10) {
// x is always a number
}
📦 Rest Parameters: Pack All the Extras!
Going on a trip? Sometimes you need to pack many items without knowing exactly how many. Rest parameters let functions accept any number of arguments!
Use ... before the parameter name.
function sum(...numbers: number[]) {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
}
sum(1, 2); // 3
sum(1, 2, 3, 4, 5); // 15
sum(); // 0 (empty is okay!)
Rest Must Be Last
// ✅ Correct: rest at the end
function invite(host: string, ...guests: string[]) {
return `${host} invited ${guests.join(", ")}`;
}
invite("Alice", "Bob", "Carol", "Dave");
// "Alice invited Bob, Carol, Dave"
Rest in Type Expressions
type MathOperation = (...nums: number[]) => number;
const multiply: MathOperation = (...nums) => {
return nums.reduce((a, b) => a * b, 1);
};
🎭 Function Overloads: One Function, Many Costumes
Imagine a superhero who looks different depending on who’s watching. Overloads let one function work differently based on what you give it!
// Overload signatures (the costumes)
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
// Implementation (the real hero)
function format(value: string | number | Date): string {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value.toFixed(2);
} else {
return value.toISOString();
}
}
format("hello"); // "HELLO"
format(3.14159); // "3.14"
format(new Date()); // "2024-01-15T..."
The Rules of Overloading
graph TD A[Overload 1] --> D[Implementation] B[Overload 2] --> D C[Overload 3] --> D D --> E[Must handle ALL overloads]
- Write overload signatures first (public faces)
- Write implementation signature last (must be compatible with all)
- Implementation signature is not visible to callers
// What callers see:
format(value: string): string
format(value: number): string
format(value: Date): string
// What they DON'T see:
// format(value: string | number | Date): string
👆 The this Parameter: Who’s Doing the Cooking?
In a kitchen, it matters who is cooking. The this parameter tells TypeScript who the function belongs to when it runs.
interface Chef {
name: string;
cook(this: Chef, dish: string): string;
}
const gordon: Chef = {
name: "Gordon",
cook(dish: string) {
return `${this.name} is cooking ${dish}`;
}
};
gordon.cook("pasta"); // "Gordon is cooking pasta"
Why Use this Parameter?
It catches mistakes when functions get separated from their objects:
interface Button {
label: string;
click(this: Button): void;
}
const myButton: Button = {
label: "Submit",
click() {
console.log(`Clicked: ${this.label}`);
}
};
// ✅ Works fine
myButton.click();
// ❌ TypeScript catches the error!
const detached = myButton.click;
detached(); // Error: 'this' context wrong
this Must Be First
// Always first, never shows up when calling
function greet(this: Person, greeting: string) {
return `${greeting}, ${this.name}`;
}
// Call with just the greeting
person.greet("Hello"); // "Hello, Alice"
🎯 Quick Summary
| Concept | What It Does | Syntax |
|---|---|---|
| Type Expression | Describes function shape | (x: T) => R |
| Call Signature | Function type with properties | { (x: T): R } |
| Construct Signature | For new keyword |
{ new (x: T): C } |
| Optional | Maybe provide it | param?: type |
| Default | Pre-set value | param = value |
| Rest | Any number of args | ...params: T[] |
| Overloads | Multiple signatures | List then implement |
| this Parameter | Define this type |
First param position |
🚀 You Did It!
You now understand how TypeScript describes functions in all their forms. Like a master chef with a complete recipe book, you can now write type-safe functions that everyone understands!
Remember:
- Type expressions = Simple recipe cards
- Signatures = Detailed recipe pages
- Optional/Default/Rest = Flexible ingredients
- Overloads = One recipe, many variations
- this = Who’s cooking matters!
Go forth and type your functions with confidence! 🎉