#Typing parameters and return types
AddedA function has two edges where types matter: what comes in (parameters) and what goes out (the return value). TypeScript treats them very differently. Parameters are where inference has nothing to go on — the compiler can't guess what a caller will pass — so you annotate them. The return type is the opposite: the compiler can read every return statement in the body and work it out, so you usually don't have to.
A useful mental model is a contract with two signatures on it. The parameter types are your promise about what you accept; the return type is your promise about what you hand back. Callers only ever see the contract, never the body. Leaving the return type to inference means "the contract is whatever the body happens to do today"; writing it down means "the body must live up to this contract", and the compiler checks the body against it.
Under strict (specifically noImplicitAny), an unannotated parameter with no context is an error — TS7006: "Parameter 'x' implicitly has an 'any' type." The exception is a callback whose type is already known from where it's passed, such as the arrow inside .map(...); that's contextual typing.
function parsePort(input: string) {
const n = Number(input);
if (Number.isNaN(n)) return null;
return n;
}
// Hover shows: function parsePort(input: string): number | null
const port = parsePort("8080");
port.toFixed(0); // ❌ 'port' is possibly 'null'.input: stringis annotated because nothing else could tell the compiler what callers will pass. Leave it off and strict mode reports TS7006 instead of quietly making itany.- The return type isn't written, so the checker collects every
returnin the body:return nullcontributesnull,return ncontributesnumber(Number(...)returnsnumber). The inferred return type is the union of them:number | null. porttherefore has typenumber | null. Callingport.toFixed(0)is rejected with TS18047: "'port' is possibly 'null'." — the compiler is reminding you about theNaNbranch you wrote yourself.- To use it you narrow first:
if (port !== null) port.toFixed(0);. This is inference doing its job well: you never wrotenumber | null, but every caller is forced to handle both outcomes.
Inference is great for small internal helpers. For functions other people call — exported functions, library APIs, module boundaries — write the return type down. The reason is where mistakes get reported:
type User = { id: number; name: string };
type Row = { id: number; full_name: string };
// No return type: the typo becomes part of the inferred type
function toUserInferred(row: Row) {
return { id: row.id, nmae: row.full_name };
}
const a: User = toUserInferred({ id: 1, full_name: "Ada" }); // ❌ Property 'name' is missing in type '{ id: number; nmae: string; }' but required in type 'User'.
// Explicit return type: the typo is caught inside the function
function toUser(row: Row): User {
return { id: row.id, nmae: row.full_name }; // ❌ Object literal may only specify known properties, and 'nmae' does not exist in type 'User'.
}toUserInferredhas no return annotation, so whatever the body returns is the type. The checker happily infers{ id: number; nmae: string }— the typo is now part of the function's public contract and nothing complains at the definition.- The error only appears at a call site that expects a
User: TS2741 "Property 'name' is missing in type '{ id: number; nmae: string; }' but required in type 'User'." In a real codebase that call site may be in another file, written by someone else, and the message points at their code rather than your bug. toUserdeclares: User. Now the returned object literal is checked againstUserright there, and excess property checking (see Excess property checking) flags the unknown key: TS2353 "Object literal may only specify known properties, and 'nmae' does not exist in type 'User'."- Same bug, two very different places to find it. The explicit return type moves the error to the line that's actually wrong — that's the main reason to annotate public functions.
- Bonus: an explicit return type is cheaper for the checker (it doesn't have to analyse the body to type a call) and is required by
isolatedDeclarations, which lets tools generate.d.tsfiles without running the full checker.
A rest parameter collects the remaining arguments into an array, so its type is always an array or tuple type. A function type expression — (a: number, b: number) => number — is how you write the type of a function value, for callbacks, variables and properties.
function sum(...nums: number[]): number {
return nums.reduce((total, n) => total + n, 0);
}
sum(); // 0
sum(1, 2, 3); // 6
sum(1, "2"); // ❌ Argument of type 'string' is not assignable to parameter of type 'number'.
type BinaryOp = (a: number, b: number) => number;
const multiply: BinaryOp = (x, y) => x * y; // x and y are number from context
const firstOnly: BinaryOp = (x) => x; // fewer parameters is fine
const tooMany: BinaryOp = (x: number, y: number, z: number) => x; // ❌ Type '(x: number, y: number, z: number) => number' is not assignable to type 'BinaryOp'....nums: number[]means "any number of arguments, each anumber". Inside,numsis a real array, soreduceworks.sum()givesnums = []and returns the initial0;sum(1, 2, 3)gives[1, 2, 3]→6.sum(1, "2"): the checker matches each extra argument against the element typenumber."2"fails with TS2345 "Argument of type 'string' is not assignable to parameter of type 'number'."type BinaryOp = (a: number, b: number) => numberdescribes a function value. The namesaandbare documentation only — an implementation can call themxandy. Becausemultiplyis annotatedBinaryOp, its parameters get their types from context, so no annotations are needed onxandy.firstOnlytakes only one parameter, and that's allowed: a function that ignores the second argument is safe to call with two. This is the same rule that lets you pass(item) => …toforEach, which actually calls it with(item, index, array).tooManyrequires a third argument thatBinaryOpcallers will never pass, sozwould always beundefined. The assignment is rejected with TS2322 and the detail line says why: "Target signature provides too few arguments. Expected 3 or more, but got 2." The rule is "the implementation may need fewer parameters than the type provides, never more". (Its parameters are annotated because contextual typing gives up on a function that doesn't fit; without annotations you'd also get three TS7006 implicit-anyerrors.)
A rest parameter can also be a tuple, which gives you a fixed, named argument list you can reuse — handy for wrapping another function (see Named, optional and variadic tuples):
type LogArgs = [level: "info" | "warn", message: string, data?: object];
function log(...args: LogArgs) {
const [level, message, data] = args;
console[level](message, data ?? "");
}
log("info", "server started");
log("warn", "slow query", { ms: 812 });
log("debug", "x"); // ❌ Argument of type '"debug"' is not assignable to parameter of type '"info" | "warn"'.LogArgsis a tuple with labels (level,message,data). Labels don't change the type; they show up in editor hints so the signature reads likelog(level, message, data?)....args: LogArgsspreads the tuple into the parameter list: position 0 must be"info" | "warn", position 1 astring, position 2 an optionalobject. Sologaccepts two or three arguments.- Inside,
argsis the tuple itself, so destructuring giveslevel: "info" | "warn",message: string,data: object | undefined.console[level]type-checks because both"info"and"warn"are methods onconsole. log("debug", "x")fails on the first element with TS2345 — the literal union rejects anything else. The sameLogArgstype could be reused by a wrapper (function logTwice(...args: LogArgs)) without repeating the parameter list.
Practical guidance:
- Annotate every parameter (except contextually typed callbacks). For optional ones, see Optional parameters vs default values.
- Annotate return types on exported functions; let inference handle small local helpers and callbacks.
- Write
voidwhen a function returns nothing on purpose (see void vs never). - For callable object types, method syntax vs property syntax, and call signatures in interfaces, see Describing functions with types and interfaces.