TypeScript optional, nullable, and default parameters

May 09, 2023#typescript#how-to

TypeScript function parameters allow you to define the types of the values that are passed into a function, which helps with type checking and improves code clarity. In addition to basic parameter syntax, TypeScript supports optional and default parameters, as well as nullable types.

These features enable you to write more flexible and robust code by allowing for the possibility of undefined or null values, and by providing default values when no value is passed in.

Function parameters

TypeScript is a superset of JavaScript that adds static types to the language. One of the benefits of TypeScript is that it allows us to specify the types of the parameters and the return value of a function, which can help us catch errors and improve readability.

A TypeScript function type is composed of the types of the parameters and the return type, separated by a => symbol. For example:

// A function type that takes a string and returns a number
type StringToNumber = (s: string) => number;

// A function that matches the type
const parseNumber = (s: string) => Number(s);

You can also use an interface or a type alias to name a function type:

// An interface that describes a function type
interface GreetFunction {
  (name: string): void;
}

// A type alias that describes a function type
type AddFunction = (x: number, y: number) => number;

You can use these named types to annotate the parameters and the return value of a function declaration or expression:

// A function declaration with a named return type
function greet(name: string): GreetFunction {
  return () => console.log(`Hello, ${name}!`);
}

// A function expression with a named parameter type
const add: AddFunction = (x, y) => x + y;

You can also use an anonymous function type to directly annotate the parameters and the return value of a function:

// A function declaration with an anonymous return type
function greet(name: string): (message: string) => void {
  return (message) => console.log(`${name}, ${message}!`);
}

// A function expression with an anonymous parameter type
const add = (x: number, y: number): number => x + y;

Optional parameters

You can mark some parameters as optional by adding a ? after their names. This means that these parameters are not required when calling the function, and their values may be undefined inside the function body. For example:

// A function with an optional parameter
function sayHello(name?: string) {
  if (name) {
    console.log(`Hello, ${name}!`);
  } else {
    console.log("Hello, stranger!");
  }
}

// We can call the function with or without an argument
sayHello("Alice"); // Hello, Alice!
sayHello(); // Hello, stranger!

Note that optional parameters must come after required parameters in a function type. For example, this is valid:

type GreetFunction = (name: string, message?: string) => void;

But this is not:

type GreetFunction = (name?: string, message: string) => void; // Error: A required parameter cannot follow an optional parameter.

Default parameters

You can also assign default values to some parameters by using the = syntax. This means that these parameters will have the default values when they are not provided when calling the function. For example:

// A function with a default parameter
function sayHello(name = "stranger") {
  console.log(`Hello, ${name}!`);
}

// We can call the function with or without an argument
sayHello("Alice"); // Hello, Alice!
sayHello(); // Hello, stranger!

Note that default parameters are also considered optional by TypeScript, so you don’t need to add a ? after their names. However, unlike optional parameters, default parameters can come before required parameters in a function type. For example:

type GreetFunction = (name = "stranger", message: string) => void;

Nullable types

You can use union types to express that a parameter can have more than one possible type. For example, you can use the | symbol to indicate that a parameter can be either a string or a number:

type StringOrNumber = string | number;

function print(value: StringOrNumber) {
  console.log(value);
}

print("hello"); // OK
print(42); // OK
print(true); // Error: Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.

One common use case of union types is to allow a parameter to have a null or undefined value. For example, you can use the | symbol to indicate that a parameter can be either a string or null:

type NullableString = string | null;

function print(value: NullableString) {
  if (value === null) {
    console.log("No value");
  } else {
    console.log(value);
  }
}

print("hello"); // OK
print(null); // OK
print(undefined); // Error: Argument of type 'undefined' is not assignable to parameter of type 'NullableString'.

Similarly, you can use the | symbol to indicate that a parameter can be either a string or undefined:

type OptionalString = string | undefined;

function print(value: OptionalString) {
  if (value === undefined) {
    console.log("No value");
  } else {
    console.log(value);
  }
}

print("hello"); // OK
print(undefined); // OK
print(null); // Error: Argument of type 'null' is not assignable to parameter of type 'OptionalString'.

You can also combine both null and undefined in a union type to allow both values:

type MaybeString = string | null | undefined;

function print(value: MaybeString) {
  if (value == null) { // This checks for both null and undefined
    console.log("No value");
  } else {
    console.log(value);
  }
}

print("hello"); // OK
print(null); // OK
print(undefined); // OK

Note that when using union types with null or undefined, you need to use type guards or type assertions to narrow down the possible types inside the function body. For example, you can use the typeof operator, the === operator, or the optional chaining operator (?.) to check for the presence of a value before accessing its properties or methods. For example:

type User = {
  name: string;
  age: number;
};

type MaybeUser = User | null | undefined;

function printUser(user: MaybeUser) {
  if (typeof user === "object" && user !== null) { // This checks for a non-null object
    console.log(`Name: ${user.name}, Age: ${user.age}`);
  } else {
    console.log("No user");
  }
}

function printUserName(user: MaybeUser) {
  if (user?.name) { // This checks for a truthy name property using optional chaining
    console.log(`Name: ${user.name}`);
  } else {
    console.log("No name");
  }
}

Combining optional, default, and nullable types

By combining these features, you can create functions that can handle a wider range of input values, and ensure that your code is more resilient to unexpected input.

Note that when you combine optional and default parameters, the default value will be used when the argument is omitted or undefined, but not when it is null. If you want the default value to be used for both null and undefined, you can use the nullish coalescing operator (??) in the function body. For example:

// A function with an optional, default, and nullable parameter
function sayHello(name: string | null = "stranger") {
  name = name ?? "stranger"; // This assigns "stranger" to name if it is null or undefined
  console.log(`Hello, ${name}!`);
}

// We can call the function with different arguments
sayHello("Alice"); // Hello, Alice!
sayHello(null); // Hello, stranger!
sayHello(); // Hello, stranger!

However it’s invalid to declare a function parameter with both a question mark (?) and an initializer (=). For example:

// This is invalid
function sayHello(name?: string = "stranger") {
  console.log(`Hello, ${name}!`);
}

This is not allowed because it is redundant and confusing. A question mark means that the parameter is optional, which means that it can be omitted or undefined when calling the function. An initializer means that the parameter has a default value, which means that it will be assigned that value when the argument is omitted or undefined. Therefore, using both a question mark and an initializer implies that the parameter can be either optional or default, which does not make sense.