Chapter 6 of 24 · TypeScript for Beginners — Complete Free Tutorial
Functions in TypeScript
Functions are where the types in the TypeScript for Beginners series finally earn their keep: they are the boundaries where data enters and leaves your logic. This chapter covers annotated parameters and return values, what strict mode demands of untyped parameters, optional, default and rest parameters, the void return type, function types for properties and callbacks, and overloads for functions that accept more than one shape.
Parameters and return types
Every parameter needs a type, because nothing else can infer it — the compiler reads declarations, not future call sites. The return type follows a second colon:
function add(a: number, b: number): number {
return a + b;
}
const sum = add(2, 3);
add("2", 3);
Expected output:
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
The return annotation is optional — const value = double(21) below infers number from the return statement — but writing it explicitly on public functions turns the signature into a promise that a careless edit inside the body cannot quietly break:
function double(n: number): number {
return n * 2;
}
const value = double(21); // inferred: number
Expected output: console.log(value) prints 42. Annotate the contract; let inference handle the interior.
Strict mode: no implicit any
The JavaScript style of leaving parameters bare does not survive strict, which you enabled in chapter 2:
function add(a, b) {
return a + b;
}
Expected output:
error TS7006: Parameter 'a' implicitly has an 'any' type.
That message is a feature, not paperwork. An implicit any is a hole in your typing: every value would be accepted and every result would be untracked, which is precisely the dynamic typing TypeScript was installed to replace. Fix it by annotating the parameters. Reaching for : any is allowed, but treat it as a loan against future debugging — if the shape is genuinely unknown, unknown plus a guard is the honest annotation.
Optional, default and rest parameters
All three JavaScript tricks work, each with a typing twist. A parameter with an initializer becomes optional at the call site, and its type is inferred from the default:
function greet(name: string, greeting = "Hello"): string {
return `${greeting}, ${name}!`;
}
console.log(greet("Ada"));
console.log(greet("Ada", "Welcome back"));
Expected output:
Hello, Ada!
Welcome back, Ada!
The default applies only when the argument is undefined — it is not a catch-all for every missing or empty value. An explicit ? marks a parameter as truly optional with no fallback, and its type inside the function includes undefined:
function tag(text: string, prefix?: string): string {
return prefix === undefined ? text : `${prefix}:${text}`;
}
function broken(message?: string, level: string) {}
Expected output:
error TS1016: A required parameter cannot follow an optional parameter.
A required parameter may never follow an optional one, so reorder them or give the second one a default: an initialized parameter counts as optional from the caller's side, which makes level = "info" resolve the conflict. Finally, a rest parameter collects any number of trailing arguments into a typed array:
function join(separator: string, ...parts: string[]): string {
return parts.join(separator);
}
console.log(join("-", "a", "b", "c"));
Expected output: a-b-c. ...parts must be last in the list, and because it is an array, everything from length to .map() type-checks as usual.
void: functions that return nothing
A function with no return statement hands back undefined; declaring : void tells callers not to use the result at all:
function announce(message: string): void {
console.log(message);
}
const returned = announce("hello");
returned.toFixed(2);
Expected output:
error TS2339: Property 'toFixed' does not exist on type 'void'.
Keep three related ideas separate: void means “no usable value is returned”, undefined is the actual value a bare return; produces, and never marks a function that does not return at all — one that always throws, such as function fail(msg: string): never { throw new Error(msg); }. One asymmetry worth memorising: a function that does return a value is still allowed where a void callback is expected, with the result simply discarded. That is what lets you pass any handler to an API that promises not to use its return.
Function types and callbacks
A whole function can be typed. In a type position the arrow => separates parameters from the return type — note it is an arrow, not the colon used in object members:
type MathFn = (n: number) => number;
const negate: MathFn = (n) => -n;
function apply(nums: number[], fn: (n: number) => number): number[] {
return nums.map((n) => fn(n));
}
console.log(apply([1, -2, 3], negate));
Expected output:
[ -1, 2, 3 ]
Two quiet powers are at work. Inside apply, n needs no annotation: the parameter's type gives the callback a context from which the compiler infers every arrow argument you pass. And the same syntax describes function-shaped members of an object type — the pattern every event handler in a UI uses:
interface Button {
label: string;
onClick: () => void;
}
Callbacks in TypeScript are simply functions typed as values, exactly as you have passed them in the JavaScript functions chapter — the annotations just make the contract explicit, so a callback that forgets to handle a parameter is caught at the call site rather than in production.
Overloads: more than one signature
Sometimes a function legitimately accepts different shapes and returns something different for each. Overloads list the public signatures first, then a single implementation wide enough for all of them:
function slugify(words: string): string;
function slugify(words: string[]): string;
function slugify(input: string | string[]): string {
const text = Array.isArray(input) ? input.join(" ") : input;
return text.toLowerCase().replace(/\s+/g, "-");
}
console.log(slugify("Hello World"));
console.log(slugify(["Hello", "World"]));
Expected output:
hello-world
hello-world
Callers see only the two overload lines; the implementation signature is hidden from them, and a call that fits neither — slugify(42) — is refused with No overload matches this call. Keep overloads few and precise: for most variation, a union parameter plus narrowing (coming up in chapter 7) is simpler to read and easier to extend.
Common mistakes
- Leaving parameters untyped — under
strictevery one of them raises TS7006; annotate the signature, not the body. - Annotating the inside instead of the contract — locals are inferred; it is the parameters and return type that callers depend on.
- Expecting a default to catch
nullor empty values — initializers apply only when the argument isundefined. - Declaring a return type with missing paths —
: numberwith an earlyreturnand no final one raiseserror TS2366: Function lacks ending return statement and return type does not include 'undefined'. - Typing a standalone arrow with no context —
const fn = (x) => x * 2has nothing to inferxfrom; annotate it as(x: number).
Practice checklist
- Write
addwith full annotations, call it wrongly, and read the TS2345 message. - Remove the parameter types from a function, trigger TS7006, then restore them.
- Build
greetwith a default andtagwith?; call each with and without the optional argument. - Add a second
restparameter and read the error, then combine everything into one...partstail. - Convert one callback-taking function to use a named
type, pass an inline arrow, and hover to see the inference at work.
What's next?
Typed functions give you the boundaries — next you learn the types that describe values with more than one possibility in Union and Intersection Types.
Frequently Asked Questions
TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.
You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.
No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.
If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.
Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.
strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.
any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.
Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.
Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.
Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.