The Strange Behavior of the void Type in TypeScript

  sonic0002        2024-09-13 23:44:26       3,761        1    

Preface

In TypeScript (TS), there is a type called void. It represents "nothing" but it's important to note that it's not the same as "nothing" in JavaScript(JS).

Typically, void is used to declare the return type of functions. Although you can declare a variable as void, we generally don't do this because it serves no meaningful purpose. Let's explore why in the examples below.

The void Type

Declaring a Variable as void

let name: void; // Declare a variable `name` with type `void`.

name = 'nanjiu'; // Error: Cannot assign type 'string' to type 'void'.
name = 18; // Error: Cannot assign type 'number' to type 'void'.
name = null; // Error: Cannot assign type 'null' to type 'void'.
name = undefined; // Valid

This means that when a variable has the type void, it can only be assigned one value: undefined. Nothing else is allowed.

Now, you can probably see why we don't typically declare variables as void. Since its only acceptable value is undefined, it doesn't serve any useful purpose in practical development.

Declaring Function Return Values as void

Explicit Return

When a function's return type is declared as void, you can return undefined inside the function:

function sayHello(): void {
  console.log('hello')
  return undefined
}

const str = sayHello()
console.log(str) // undefined

Besides undefined, no other values can be returned.

Implicit Return

In JS, when you don’t explicitly return a value from a function, the implicit return value is undefined. This makes the following code valid:

function sayHello(): void {
  console.log('hello')
}

const str = sayHello()
console.log(str) // undefined

You Should Not Rely on void Values

Another key feature of void is that callers should not rely on its return value for any operations. For example:

let name: void; // Declare a variable `name` with type `void`.

// Function with return type `void`
function sayHello(): void {
  console.log('hello');
}

// Return value of function is `void`, i.e., `undefined`
const str = sayHello();
console.log(str); // undefined

// Error: Can't test the truthiness of a "void" expression.
if (str) {
  console.log('str exists');
} else {
  console.log('str does not exist');
}

Because void in TS means "nothing", so you shouldn't use it for any operations.

In summary:

  • void is generally used to declare the return type of functions, and it signifies "nothing. The only acceptable value for a void type is undefined.
  • You should not rely on the return value of a void type for any operations.

This is quite simple and boils down to these two key points, but the following example might surprise you a bit...

type

it is a keyword in TS used to create custom types. It allows us to create aliases for any type, making type reuse and extension more convenient.

For example:

// Create a custom type that can be either a string or a number
type strOrnum = string | number;

// Declare a variable of type `strOrnum`
let str: strOrnum;
str = 'nanjiu'; // Can be assigned a string
str = 18; // Can also be assigned a number

Of course, type has many powerful features like union types, intersection types, etc., but we won’t dive into those here. Let's look at an interesting problem:

Declaring Function Types

// Create a function type, where the parameter is a string and the return value is `void`
type say = (name: string) => void;

// Define a function with type `say`
let sayHello: say = (name: string) => {
  console.log(`hello ${name}`);
};

sayHello('nanjiu');

Here, we used type to create a function type. The function takes one parameter of type string and returns a value of type void.

Functions Returning Non-undefined Values

From the explanation above, we know that a function with a void return type can only return undefined (either explicitly or implicitly). However, in this case, you can return any value...

// Create a function type, where the parameter is a string and the return value is `void`
type say = (name: string) => void;

// Define a function with type `say`
let sayHello: say = (name: string) => {
  console.log(`hello ${name}`);
  return null;
};

const res = sayHello('nanjiu');
console.log(res); // null

Why does this happen? At first glance, this example seems to contradict the definition of void. Is this a bug in TS?

Actually, it’s not. The official explanation is:

This behavior exists to ensure that the following code is valid. We know that Array.prototype.push returns a number, while Array.prototype.forEach expects its callback to have a return type of void.

const arr = [1, 2, 3, 4, 5]
const list = [0]

arr.forEach(item => list.push(item))
console.log(list)

It's effectively the same as using type for a custom type declaration:

type callbackfn = (value: number, index: number, array: number[]) => void

This function is defined to have three parameters: the first two are of type number, and the third is an array of numbers. The return type of the function is void.

Since our callback function uses the arrow function shorthand, it implicitly returns list.push(item). The push method, however, does have a return value.

item => list.push(item)

is equivalent to:

item => {
  return list.push(item)
}

which is equivalent to:

item => {
  return 2
  
  // return 3
  // return 4
  // ...
}

This means the function’s return type becomes number, which doesn't match the void return type definition.

So, to allow this shorthand syntax, TypeScript behaves this way. When a function's return type is restricted to void, TS won't strictly enforce that the function returns nothing.

Otherwise, we would have to write the following:

arr.forEach(item => {
  list.push(item);
});
// or
arr.forEach(function(item) {
  list.push(item);
});

However, even though TS doesn't strictly enforce that a void return type must return "nothing," we still cannot rely on its return value for any operations.

// Create a function type, where the parameter is a string and the return value is `void`
type say = (name: string) => void;

// Define a function with type `say`
let sayHello: say = (name: string) => {
  console.log(`hello ${name}`);
  return name;
};

const res = sayHello('nanjiu');
console.log(res); // 'nanjiu'

if (res) {
  console.log('res');
}

Reference: https://segmentfault.com/a/1190000045248514

JAVASCRIPT  TYPE  TYPESCRIPT  VOID 

       

  RELATED


  1 COMMENT


Anonymous [Reply]@ 2024-09-15 11:30:32

Isn't this just covariance with a top type?