Typing Function Parameters & Returns
Type your inputs, know your outputs
Functions are the heart of any program. They're where the real work happens — data comes in, processing occurs, and results come out. In JavaScript, functions accept any arguments and return anything. That's chaos! You could call add("hello", true) and JavaScript would silently try to make it work, producing bizarre results at runtime.
TypeScript brings order to this chaos by letting you specify exactly what type each parameter must be and what type the function returns. Think of it like ordering at the Biryani counter at Shadab Hotel. When you say "One chicken dum biryani," you're giving a typed parameter — the chef knows EXACTLY what to cook. There's no ambiguity, no confusion. And when the waiter brings your plate, that's the return type — you EXPECT a plate of biryani, not a cup of chai. The contract is clear.
If you order biryani but pass "cold coffee" as your order, the system should reject it immediately. The chef shouldn't have to figure out what you meant. That's exactly what TypeScript does for your functions — it checks that what you pass IN matches what the function expects, and what comes OUT matches what the caller expects.
No surprises. No "bhai yeh kyun aaya?" moments. The type annotations form a contract between the caller and the function. The function promises: "If you give me these types of inputs, I guarantee I'll return this type of output." And TypeScript enforces that promise at compile time, before your code ever runs. This is the foundation of writing reliable, maintainable TypeScript code. Everything else in this course builds on top of this concept.
In this chapter, you'll learn how to type parameters, how to declare return types, and how to store entire function type signatures as variables. Let's get started.
Every function parameter can have a type annotation. The syntax is simple — just add a colon and the type after the parameter name: function greet(name: string) { ... }. TypeScript will now check every call to this function and make sure the argument matches the declared type.
Basic parameter typing:
function add(a: number, b: number) {
return a + b;
}
function isAdult(age: number) {
return age >= 18;
}
When you call add(3, 5), TypeScript is happy — both arguments are numbers. But if you call add("hello", 2), TypeScript immediately flags an error: Argument of type 'string' is not assignable to parameter of type 'number'. The protection kicks in before the code ever runs.
Multiple parameters with different types:
function createUser(name: string, age: number, isActive: boolean) {
console.log(name, age, isActive);
}
createUser("Imran", 25, true); // ✅ Correct
createUser(25, "Imran", true); // ❌ Error — types don't match positions
createUser("Imran", 25); // ❌ Error — missing argument
TypeScript checks the NUMBER of arguments too. Unlike JavaScript, which silently passes undefined for missing arguments, TypeScript requires all declared parameters to be provided. Calling add(1) with a missing second argument is a compile-time error. This catches a huge category of bugs that JavaScript would let slip through silently.
Arrow functions with typed parameters:
const multiply = (a: number, b: number): number => a * b;
const greet = (name: string): string => "Hello, " + name;
The type annotations work exactly the same way for arrow functions — parameters get their types with colons, and the return type comes before the arrow.
Contextual typing — when TypeScript figures it out for you:
const names: string[] = ["Hyderabad", "Mumbai", "Delhi"];
// TypeScript KNOWS 'item' is a string — no annotation needed!
const upper = names.map((item) => item.toUpperCase());
// TypeScript KNOWS 'num' is a number
const numbers: number[] = [1, 2, 3];
const doubled = numbers.map((num) => num * 2);
When you use callbacks with .map(), .filter(), .reduce(), TypeScript can infer the parameter types from the context. It knows the array is string[], so the callback parameter must be string. This is called contextual typing, and it saves you from writing redundant annotations. You can still write them explicitly if you want, but it's not necessary when the context provides enough information.
Object parameters:
function printUser(user: { name: string; age: number }) {
console.log(user.name + " is " + user.age);
}
printUser({ name: "Imran", age: 25 }); // ✅
printUser({ name: "Imran" }); // ❌ Error — missing 'age'
You can type object parameters inline with the curly brace syntax. Each property gets its own type annotation, and TypeScript checks that all required properties are present in the argument.
Every function returns something, and TypeScript is smart enough to figure out the return type automatically through type inference. But you can also explicitly annotate the return type, and there are good reasons to do so.
Implicit return type (inference):
function add(a: number, b: number) {
return a + b; // TypeScript infers return type: number
}
TypeScript looks at the return statement, sees a + b where both are number, and infers the return type as number. You don't have to write it explicitly.
Explicit return type annotation:
function add(a: number, b: number): number {
return a + b;
}
The return type annotation goes between the closing parenthesis and the opening brace: ): returnType {. This makes the contract crystal clear and enforces it from both sides.
When should you use explicit return types?
- Public APIs — When a function is part of a library or shared module, an explicit return type enforces the contract. If someone accidentally changes the return value, TypeScript catches it immediately. The annotation becomes documentation that the compiler verifies.
- Large functions — When a function has many branches and returns from multiple places, inference might surprise you. An explicit return type ensures every
returnstatement produces the right type. - Functions that return different types from different branches — Without an explicit annotation, TypeScript infers a union type, which might not be what you intended.
Wrong return types are caught immediately:
function getName(): string {
return 42; // ❌ Error! Type 'number' is not assignable to type 'string'
}
function getAge(): number {
return "twenty"; // ❌ Error! Type 'string' is not assignable to type 'number'
}
TypeScript checks every return statement inside the function body against the declared return type. If any return doesn't match, you get an error. This is incredibly powerful for catching bugs where you accidentally return the wrong type from an if/else branch.
Void return type — when a function returns nothing:
function logMessage(msg: string): void {
console.log(msg);
// No return statement — return type is void
}
When a function doesn't return a value, its return type is void. This means "nothing meaningful is returned." We'll cover void in more depth in Chapter 2.6, but for now just know that void is the return type for functions that perform actions without producing a result.
Returning objects:
function getUser(): { name: string; age: number } {
return { name: "Imran", age: 25 }; // ✅ Matches the return type
}
function getBadUser(): { name: string; age: number } {
return { name: "Imran" }; // ❌ Error — missing 'age' property
}
Object return types use the same inline syntax as object parameters. TypeScript checks that the returned object has all the required properties with the correct types.
Returning arrays:
function getIds(): number[] {
return [1, 2, 3]; // ✅ Array of numbers
}
function getLandmarks(): string[] {
return ["Charminar", "Golconda", "Hussain Sagar"]; // ✅
}
Array return types use the familiar type[] syntax. TypeScript verifies that every element in the returned array matches the declared element type.
Sometimes you need to describe the shape of a function itself — not a specific function, but the type that any compatible function must follow. TypeScript lets you store a function's type signature in a variable declaration, which is essential for callbacks, event handlers, and higher-order functions.
Basic function type syntax:
let myFunc: (a: number, b: number) => number;
This reads: "myFunc must be a function that takes two number parameters and returns a number." It's a contract — any function assigned to myFunc must match this signature exactly.
function add(a: number, b: number) {
return a + b;
}
function greet(name: string) {
return "Hello, " + name;
}
myFunc = add; // ✅ add takes two numbers, returns number
myFunc = greet; // ❌ Error! greet takes a string, doesn't match
TypeScript checks that the assigned function's parameter types, parameter count, and return type all match the declared signature. If anything is off, you get an error at the assignment — not at runtime.
Parameter names in function type declarations are for readability only:
// These are the SAME type — parameter names don't matter
let fn1: (x: number, y: number) => number;
let fn2: (a: number, b: number) => number;
fn1 = fn2; // ✅ Perfectly fine — same signature
The parameter names x and y vs a and b make no difference. Only the types and their order matter. Use names that make the type readable — like (price: number, quantity: number) => number instead of (a: number, b: number) => number.
Callback types: This is where function type signatures shine. When a function accepts another function as a parameter, you need to type that callback:
function fetchData(callback: (data: string) => void) {
// Simulate fetching data
callback("Hello from server");
}
fetchData((result) => {
console.log(result); // TypeScript knows result is string
});
The callback type (data: string) => void tells TypeScript: "This parameter must be a function that receives a string and returns nothing." When you call fetchData and pass a callback, TypeScript contextually types the callback's parameter — so result is automatically inferred as string without you having to annotate it.
Typing event handlers — a real-world use case:
type ClickHandler = (event: MouseEvent) => void;
function addButtonListener(handler: ClickHandler) {
// ...attach handler to button
}
addButtonListener((e) => {
console.log(e.clientX, e.clientY); // TypeScript knows e is MouseEvent
});
Higher-order functions — Array methods:
const names: string[] = ["Hyderabad", "Mumbai"];
// map's callback: (value: string, index: number, array: string[]) => U
const lengths = names.map((name) => name.length); // number[]
// filter's callback: (value: string, index: number, array: string[]) => boolean
const long = names.filter((name) => name.length > 5); // string[]
These are all powered by function type signatures under the hood. TypeScript knows the exact type of each callback parameter because Array.prototype.map and Array.prototype.filter are typed with function type signatures. This is why you get autocomplete and type checking inside callbacks without writing any annotations yourself.
Here's your complete cheatsheet for function type annotations in TypeScript. Pin this to your desk — you'll reference it constantly.
Parameter Types:
// Basic parameter typing
function greet(name: string) { ... }
// Multiple typed parameters
function add(a: number, b: number) { ... }
// Mixed parameter types
function createUser(name: string, age: number, active: boolean) { ... }
// Object parameter
function printUser(user: { name: string; age: number }) { ... }
Return Types:
// Explicit return type
function add(a: number, b: number): number { return a + b; }
// Void return (no return value)
function log(msg: string): void { console.log(msg); }
// Object return type
function getUser(): { name: string; age: number } {
return { name: "Imran", age: 25 };
}
// Array return type
function getIds(): number[] { return [1, 2, 3]; }
Arrow Functions:
const add = (a: number, b: number): number => a + b;
const greet = (name: string): string => "Hello, " + name;
const log = (msg: string): void => { console.log(msg); };
Function Type Variables:
// Variable with function type
let myFunc: (a: number, b: number) => number;
// Callback type
function fetchData(cb: (data: string) => void) { ... }
// Type alias for function types (coming in later chapters)
type MathOp = (a: number, b: number) => number;
Key Rules:
- TypeScript checks arguments at the call site — wrong types = error
- TypeScript checks return statements inside the function body — wrong returns = error
- Fewer arguments than parameters = error (no silent undefined like JS)
- More arguments than parameters = error
- Return type annotation is optional when TypeScript can infer it, but recommended for public APIs
- Parameter names in function type declarations are for readability only — only types and order matter
The Golden Rule:
"Type your inputs, know your outputs — that's the Biryani Box contract. What goes in and what comes out should never be a surprise."
Common mistakes to avoid:
- Forgetting to annotate parameters — TypeScript defaults to
anyfor untyped parameters in strict mode, which is an error - Confusing the return type position — it goes before the opening brace, not after
- Not using explicit return types for exported/public functions — inference is convenient but fragile for APIs
- Thinking parameter names in type signatures matter — they don't, only types and order do
Key Points
- Type each parameter with `: type` — `function f(name: string)`
- Return type goes before the body — `function f(): number { ... }`
- TS checks argument types at call site AND return types inside the body
- Function type variables: `let fn: (a: number, b: number) => number`
- Parameter names in type signatures are for readability — only types and order matter
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login