Loading
All articles
Programming 14 min read

TypeScript Function Parameter Types: A Visual, Hands-On Guide

Learn TypeScript function parameter types visually: why add("3", 5) prints 35, your first annotation, what tsc really does, optional, default and rest parameters, unions and narrowing, object parameters, any vs. unknown, what types can't catch, and exercises.

nbkhope
On this page

Adding two numbers should be the easiest function you ever write. In JavaScript, though, nothing stops someone from passing a string by accident, and the program won't complain. It just gives you a wrong answer and keeps going.

TypeScript is JavaScript with types. You state what each function parameter should be, and the compiler checks every call before the program runs.

In this guide you'll write your first annotation and see what the compiler really does, including one thing that surprises most beginners. You'll then cover optional, default and rest parameters, union types, object parameters, the difference between any and unknown, and what types can't protect you from. Exercises wrap it up.

All outputs below come from real runs with TypeScript 7.0 and Node.js 26 on Windows. New to JavaScript? Start with What Is the JavaScript Programming Language?

The bug TypeScript catches

Here's a small function in plain JavaScript, with an honest mistake in the call:

1function add(first, second) {
2  return first + second;
3}
4
5console.log(add("3", 5));
35

3 plus 5 should be 8. The first argument, however, is the text "3", and in JavaScript + joins a string and a number into the string "35". There's no error at all, just a wrong value that can travel far from the line that caused it.

Setting up

You need Node.js. In a new folder, install the TypeScript compiler, called tsc:

npm init -y
npm install --save-dev typescript

From now on, write your code in a file called main.ts, then compile and run it with two commands:

npx tsc main.ts
node main.js
TypeScript checks your code, then removes the types. main.ts, the code with types, goes into tsc through the command npx tsc main.ts. tsc checks the types, reports any type errors before you run, and writes main.js, which is plain JavaScript. Then node main.js runs the program. The types exist only up to this point, and nothing checks them while the program runs

tsc does two jobs: it checks your types, and it writes a plain JavaScript file. Node.js then runs that file. Most code editors run the same checks as you type, so you'll usually see the errors as red underlines before you compile anything.

Your first annotation

To give a parameter a type, write a colon and the type after its name:

1function add(first: number, second: number) {
2  return first + second;
3}
4
5console.log(add("3", 5));
main.ts(5,17): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

The compiler found the bug without running anything. The message tells you the file, the line and column (5,17), an error code and the problem. The fix is to pass a number:

1function add(first: number, second: number) {
2  return first + second;
3}
4
5console.log(add(3, 5));
8

A type error doesn't stop the compiler

Here's the surprise. Go back to the broken call, compile it, and look in your folder. tsc reported the error, and it wrote main.js anyway:

1"use strict";
2function add(first, second) {
3    return first + second;
4}
5console.log(add("3", 5));

The types are gone, and running that file still prints the wrong answer:

35

So a type error is a warning you have to act on. It doesn't stop the program by itself. Two details follow from this:

  • To refuse to write a file when there are errors, run npx tsc --noEmitOnError main.ts. With that flag, the broken version produced no main.js.
  • Node.js can run .ts files directly, but it doesn't check them. Recent versions simply remove the types, so node main.ts printed 35 for the broken version as well, with no warning. You still need tsc for the checking.

Eight bad calls

How much does this catch? We tried eight wrong calls to the same add function:

1function add(first: number, second: number) {
2  return first + second;
3}
4
5console.log(add("3", 5));
6console.log(add(3));
7console.log(add(3, 5, 7));
8console.log(add(null, 5));
9console.log(add(undefined, 5));
10console.log(add([1], 5));
11console.log(add({}, 5));
12console.log(add(true, 5));
Eight bad calls, zero errors from JavaScript. add("3", 5) prints 35, add(3) prints NaN, add(3, 5, 7) prints 8, add(null, 5) prints 5, add(undefined, 5) prints NaN, add([1], 5) prints 15, add({}, 5) prints [object Object]5, and add(true, 5) prints 6. TypeScript reports all eight: error TS2554 for the two calls with the wrong number of arguments, and TS2345 for the six with the wrong type

JavaScript ran all eight without a single error. TypeScript rejected all eight. Some of those results look believable, too: add(null, 5) gives 5, and add(3, 5, 7) gives 8 while silently ignoring the 7.

Common parameter types

The basic types are string, number and boolean, and you write an array by adding []:

1function describePlayer(name: string, level: number, active: boolean, scores: number[]) {
2  const total = scores.reduce((sum, score) => sum + score, 0);
3  return `${name} (level ${level}, ${active ? "active" : "away"}): ${total} points`;
4}
5
6console.log(describePlayer("Ana", 3, true, [10, 20, 5]));
Ana (level 3, active): 35 points

Notice that sum and score have no annotations. TypeScript knows scores is a number[], so it works out the types of the callback's parameters by itself.

Leaving out the type

For an ordinary function, TypeScript can't guess a parameter's type, and it won't accept a silent guess either:

1function add(first, second) {
2  return first + second;
3}
4
5console.log(add(3, 5));
main.ts(1,14): error TS7006: Parameter 'first' implicitly has an 'any' type.
main.ts(1,21): error TS7006: Parameter 'second' implicitly has an 'any' type.

This check comes from strict mode, which is on by default in TypeScript 7. Older versions need asking: with TypeScript 5.9, plain tsc accepted this file without a word. If your project uses an older version, add --strict or set "strict": true in tsconfig.json.

Optional, default and rest parameters

Every parameter is required unless you say otherwise. There are three ways to say otherwise:

Four kinds of function parameter. name: string is required, so the caller must pass a string. greeting?: string is optional, so it may be left out, and is then undefined. times = 2 has a default, which is used when it's left out, and its type is inferred. ...numbers: number[] is a rest parameter, which collects any number of arguments in an array

An optional parameter has a ? after its name. Inside the function it might be undefined, and TypeScript makes you deal with that:

1function greet(name: string, greeting?: string) {
2  return `${greeting.toUpperCase()}, ${name}`;
3}
4
5console.log(greet("Ana", "Hi"));
main.ts(2,13): error TS18048: 'greeting' is possibly 'undefined'.

Check for it first, and the error goes away:

1function greet(name: string, greeting?: string) {
2  if (greeting === undefined) {
3    return `Hello, ${name}`;
4  }
5  return `${greeting.toUpperCase()}, ${name}`;
6}
7
8console.log(greet("Ana"));
9console.log(greet("Ana", "Hi"));
Hello, Ana
HI, Ana

Optional parameters must come after the required ones:

1function greet(greeting?: string, name: string) {
2  return `${greeting}, ${name}`;
3}
4
5console.log(greet("Hi", "Ana"));
main.ts(1,35): error TS1016: A required parameter cannot follow an optional parameter.

A default value is often simpler, and it needs no annotation, because the type comes from the value. A rest parameter collects the remaining arguments into an array:

1function repeat(text: string, times = 2) {
2  return text.repeat(times);
3}
4
5function total(...numbers: number[]) {
6  return numbers.reduce((sum, number) => sum + number, 0);
7}
8
9console.log(repeat("ha"));
10console.log(repeat("ha", 3));
11console.log(total(1, 2, 3));
haha
hahaha
6

Accepting more than one type

A union type, written with |, accepts any of the listed types. You can only use what all of them have in common, though:

1function format(value: string | number) {
2  return value.toUpperCase();
3}
4
5console.log(format("hi"));
main.ts(2,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
  Property 'toUpperCase' does not exist on type 'number'.

Test the type with typeof, and TypeScript follows your logic. This is called narrowing:

1function format(value: string | number) {
2  if (typeof value === "string") {
3    return value.toUpperCase();
4  }
5  return value.toFixed(2);
6}
7
8console.log(format("hi"));
9console.log(format(3.14159));
HI
3.14

Inside the if, value is a string. After it, only number is left.

A union can also list exact values. That's perfect for a parameter with a few valid options:

1function sortNames(names: string[], direction: "asc" | "desc") {
2  const sorted = [...names].sort();
3  return direction === "asc" ? sorted : sorted.reverse();
4}
5
6console.log(sortNames(["Bruno", "Ana"], "down"));
main.ts(6,41): error TS2345: Argument of type '"down"' is not assignable to parameter of type '"asc" | "desc"'.

Object parameters

Describe an object's shape once with a type, and then use its name:

1type Student = {
2  name: string;
3  grade: number;
4};
5
6function describe(student: Student) {
7  return `${student.name}: ${student.grade}`;
8}
9
10console.log(describe({ name: "Ana", grade: 9 }));
11console.log(describe({ name: "Ana", grade: 9, age: 20 }));
12console.log(describe({ name: "Ana" }));
main.ts(11,47): error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'Student'.
main.ts(12,22): error TS2741: Property 'grade' is missing in type '{ name: string; }' but required in type 'Student'.

Both a missing property and a misplaced extra one are caught, which also protects you from typos in property names. You can pull the properties out right in the parameter list, too:

1type Student = {
2  name: string;
3  grade: number;
4};
5
6function describe({ name, grade }: Student) {
7  return `${name}: ${grade}`;
8}
9
10console.log(describe({ name: "Ana", grade: 9 }));
Ana: 9

any and unknown

Two special types accept every value, and they behave in opposite ways. any turns checking off:

1function shout(value: any) {
2  return value.toUpperCase();
3}
4
5console.log(shout(5));
TypeError: value.toUpperCase is not a function

That compiled with no errors and crashed when it ran, which is exactly what TypeScript is meant to prevent. unknown also accepts everything, but it makes you check before you use the value:

1function shout(value: unknown) {
2  return value.toUpperCase();
3}
4
5console.log(shout(5));
main.ts(2,10): error TS18046: 'value' is of type 'unknown'.

Add the check, and it's safe:

1function shout(value: unknown) {
2  if (typeof value === "string") {
3    return value.toUpperCase();
4  }
5  return "not text";
6}
7
8console.log(shout("hi"));
9console.log(shout(5));
HI
not text

Prefer unknown when you don't know a value's type, and avoid any unless you have no other choice.

What types can't catch

Types are removed before the program runs, so they say nothing about data that arrives at run time. JSON.parse returns any, which brings the original bug straight back:

1function add(first: number, second: number) {
2  return first + second;
3}
4
5const input = JSON.parse('{"first": "3", "second": 5}');
6console.log(add(input.first, input.second));
35

This compiles with no errors and prints 35. Data from a file, a form or an API needs a real check while the program runs:

1function toNumber(value: unknown) {
2  if (typeof value !== "number") {
3    throw new Error(`Expected a number, got ${typeof value}`);
4  }
5  return value;
6}
7
8function add(first: number, second: number) {
9  return first + second;
10}
11
12const input = JSON.parse('{"first": "3", "second": 5}');
13
14try {
15  console.log(add(toNumber(input.first), toNumber(input.second)));
16} catch (error) {
17  console.log(String(error));
18}
Error: Expected a number, got string

Check the data where it enters your program, and from there on the types can protect you.

Your turn: practice

Try these before peeking at the answers. Save each one as main.ts, then run npx tsc main.ts and node main.js.

Exercise 1: Find the errors (warm-up). Which of these calls does TypeScript reject, and why?

1function repeat(text: string, times?: number) {
2  return text.repeat(times ?? 1);
3}
4
5console.log(repeat("a"));
6console.log(repeat("a", 2));
7console.log(repeat(2, "a"));
8console.log(repeat("a", null));

Exercise 2: Average. Write average(numbers: number[]), which returns the average of an array. Print the average of 1, 2 and 3.

Exercise 3: Predict both tools. What does tsc report for this program, and what does node main.js print afterward?

1function double(value: number) {
2  return value * 2;
3}
4
5console.log(double("4"));

Exercise 4: Full name. Write fullName(first: string, last?: string), which returns just the first name when there's no last name.

Exercise 5: Discount (challenge). Define a Product type with a name and a price. Write applyDiscount(product: Product, percent = 10), which returns text like Book: 90. Try it on a book that costs 100, with the default discount and with 25%.

Quick quiz

  1. How do you say that a parameter must be a number?
  2. Does tsc write a JavaScript file when it finds a type error?
  3. Does node main.ts check your types?
  4. What's the difference between greeting?: string and greeting = "Hello"?
  5. What's the difference between any and unknown?
  6. Can TypeScript catch a wrong type in data from JSON.parse?

Answers

Exercise 1: The first two calls are fine. In the third, the first argument is the wrong type, and in the fourth, null isn't allowed, because an optional parameter accepts number or undefined:

main.ts(7,20): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
main.ts(8,25): error TS2345: Argument of type 'null' is not assignable to parameter of type 'number | undefined'.

Exercise 2: The array's type also gives sum and number their types:

1function average(numbers: number[]) {
2  const total = numbers.reduce((sum, number) => sum + number, 0);
3  return total / numbers.length;
4}
5
6console.log(average([1, 2, 3]));
2

Exercise 3: tsc reports the string, then writes main.js anyway. When it runs, JavaScript's * turns "4" into a number, so the answer happens to be right:

main.ts(5,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
8

Exercise 4: Check for undefined, which narrows last to a string:

1function fullName(first: string, last?: string) {
2  return last === undefined ? first : `${first} ${last}`;
3}
4
5console.log(fullName("Ana"));
6console.log(fullName("Ana", "Silva"));
Ana
Ana Silva

Exercise 5: The default value gives percent its type:

1type Product = {
2  name: string;
3  price: number;
4};
5
6function applyDiscount(product: Product, percent = 10) {
7  const price = product.price * (1 - percent / 100);
8  return `${product.name}: ${price}`;
9}
10
11const book = { name: "Book", price: 100 };
12
13console.log(applyDiscount(book));
14console.log(applyDiscount(book, 25));
Book: 90
Book: 75

Quiz: (1) With a colon after its name: first: number. (2) Yes, unless you use --noEmitOnError. (3) No. It removes the types and runs the code. (4) The optional one is undefined when it's left out, and the default one gets the value "Hello". (5) any turns checking off, while unknown makes you check the type before you use the value. (6) No. JSON.parse returns any, so you need a check that runs with the program.

Cheat sheet

I want to…Write
Install the compilernpm install --save-dev typescript
Check and compilenpx tsc main.ts
Refuse to write a file on errorsnpx tsc --noEmitOnError main.ts
Type a parameterfirst: number
Accept an arrayscores: number[]
Make a parameter optionalgreeting?: string
Give a default valuetimes = 2
Accept any number of arguments...numbers: number[]
Accept several typesvalue: string | number
Accept only certain valuesdirection: "asc" | "desc"
Narrow a unionif (typeof value === "string") { … }
Describe an objecttype Student = { name: string; grade: number };
Unpack an object parameterfunction describe({ name, grade }: Student)
Accept anything, safelyvalue: unknown

Wrapping up

A parameter type is a promise that the compiler checks at every call:

  • A colon and a type annotate a parameter, as in first: number.
  • tsc checks your types and writes plain JavaScript. Types don't exist when the program runs.
  • A type error doesn't stop tsc from writing the file, unless you use --noEmitOnError.
  • node main.ts runs your code without checking it.
  • Strict mode rejects parameters with no type. It's on by default in TypeScript 7.
  • Use ?, default values and ... for flexible parameters.
  • Unions accept several types, and typeof narrows them.
  • type describes objects, which catches missing and extra properties.
  • Prefer unknown to any.
  • Check outside data at run time. Types can't see what arrives while the program runs.

Next up: function return types in TypeScript.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 4 min read

Ruby Blocks and Iterators: Write Your Own

Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

nbkhope
Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.