Skip to content
aviral gupta

// B4.4 · ~30 min · Beginner

void, unknown, never and this

After this lesson you can type what a function returns when it returns nothing useful, accept any object or value safely, and declare what this is.

Lesson 4 of 5 in B4 More on functions

You will be able to

  • Use void as a return type, and explain why a void function type accepts functions that return a value
  • Choose object, unknown, never or a function type over Object, Function and any in a signature
  • Declare the type of this in a function, and use a function expression where this matters
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: neither function returns a value. What does this print?

    function noop() {
      return;
    }
    
    const values = [noop(), [1, 2].forEach((n) => n * 2)];
    console.log(values);
  2. Predict · Activity 2 of 7

    Predict before you read on. Both functions return true where void is written. Which line does tsc reject?

    type VoidFunc = () => void;
    
    const f1: VoidFunc = () => true;
    
    function f2(): void {
      return true;
    }
  3. Practice · Activity 3 of 7

    One listener logs, the other pushes, and push returns a number. Fill in the return type of Listener, so that both fit and the program prints got save, then 1.

    type Listener = (event: string) => ____;
    
    const seen: string[] = [];
    const listeners: Listener[] = [(e) => console.log("got " + e), (e) => seen.push(e)];
    listeners.forEach((listener) => listener("save"));
    console.log(seen.length);
    type Listener = (event: string) => ;
  4. Practice · Activity 4 of 7

    keys takes an object, keys2 an Object. Which line does tsc reject?

    function keys(o: object): string[] {
      return Object.keys(o);
    }
    function keys2(o: Object): string[] {
      return Object.keys(o);
    }
    
    console.log(keys({ a: 1 }), keys([5]), keys(() => 1));
    console.log(keys2(42));
    console.log(keys(42));
  5. Practice · Activity 5 of 7

    increment declares what this must be. Which line does tsc reject?

    const counter = {
      count: 0,
      increment(this: { count: number }) {
        this.count++;
        return this.count;
      },
    };
    console.log(counter.increment());
    const inc = counter.increment;
    inc();
  6. Brain teaser · Activity 6 of 7

    Brain teaser. greet is taken off user and called on its own. What happens when you run npx tsc, then node main.ts?

    const user = {
      name: "Ada",
      greet() {
        return "Hi, " + this.name;
      },
    };
    const greet = user.greet;
    console.log(user.greet());
    console.log(greet());
  7. Apply · Activity 7 of 7

    Mini-task. Write a type LineCallback for a function that takes a line and its index and returns void, and eachLine(text, callback), which calls it for each line of the text. Pass it a callback that pushes each line into an array: push returns a number, which must be accepted. Write readJson(text) with the return type unknown, and check the result before you use it. Finally, an object counter with a method count(this: { lines: number }, line: string) that counts non-empty lines. Check it in the editor, or with npx tsc and node main.ts.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Admins, settings and keys

The Handbook's DB, as an interface: filterUsers calls its callback with a User as this, and the callback is a function expression that declares this: User. forEach takes a void callback, and push's number is ignored. safeParse returns unknown, so the program checks the value before reading theme. countKeys takes an object. Run npx tsc, then node main.ts. Then replace the callback with () => this.admin and run tsc again.

main.ts

interface User {
  name: string;
  admin: boolean;
}

// filter is called with a User as this: declare it, as the Handbook's DB does.
interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

const users: User[] = [
  { name: "Ada", admin: true },
  { name: "Grace", admin: false },
  { name: "Linus", admin: true },
];

const db: DB = {
  filterUsers(filter) {
    return users.filter((user) => filter.call(user));
  },
};

// A function expression, not an arrow: an arrow would not get this from the call.
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

// A void callback may return something: push returns a number, which forEach ignores.
const names: string[] = [];
admins.forEach((user) => names.push(user.name));
console.log("admins: " + names.join(", "));

// unknown: the caller must check the value before using it.
function safeParse(s: string): unknown {
  return JSON.parse(s);
}
const settings = safeParse('{"theme": "dark"}');
if (typeof settings === "object" && settings !== null && "theme" in settings) {
  console.log("theme: " + String(settings.theme));
}

// object accepts arrays and functions too, but no primitives.
function countKeys(value: object): number {
  return Object.keys(value).length;
}
console.log(countKeys({ a: 1, b: 2 }), countKeys(["x"]), countKeys(() => 0));

Run it with

npx tsc
node main.ts

Output

admins: Ada, Linus
theme: dark
2 1 0
  • filter.call(user) calls the callback with user as this, as the type (this: User) => boolean promises.
  • With () => this.admin, tsc reports error TS2532: Object is possibly 'undefined': an arrow function takes this from outside, and outside any function in main.ts there is none.
  • Without the checks, settings.theme would be error TS18046: 'settings' is of type 'unknown'.
  • An array has one key per element, and a function value has no own enumerable keys, hence 1 and 0.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

An event hub with void listeners

A Listener receives a message. The two listeners at the bottom push into history, and push returns a number, so tsc reports error TS2322: Type 'number' is not assignable to type 'undefined'. for each of them. Change Listener so that any listener fits, whatever it returns. Give on the return type void. Then make emit return how many listeners it called, with the return type number.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Which return type does the Handbook use for a callback whose result is ignored?

  2. Hint 2

    export type Listener = (message: string) => void; then push's number is simply ignored.

  3. Hint 3

    In emit, after the forEach: return listeners.length; and write : number after the parameter list.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export type Listener = (message: string) => void;

const listeners: Listener[] = [];
export const history: string[] = [];

export function on(listener: Listener): void {
  listeners.push(listener);
}

export function emit(message: string): number {
  listeners.forEach((listener) => listener(message));
  return listeners.length;
}

on((message) => history.push(message));
on((message) => history.push(message.toUpperCase()));
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export type Listener = (message: string) => undefined;

const listeners: Listener[] = [];
export const history: string[] = [];

export function on(listener: Listener) {
  listeners.push(listener);
}

export function emit(message: string) {
  listeners.forEach((listener) => listener(message));
}

on((message) => history.push(message));
on((message) => history.push(message.toUpperCase()));

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {on, emit, history} from './main.ts';

test('emit calls both listeners and returns 2', () => {
  const called = emit('hi');
  assert.equal(called, 2, `emit('hi') returned ${called}`);
});

test('the listeners wrote the message into history', () => {
  assert.deepEqual(history.slice(0, 2), ['hi', 'HI'], `history starts with ${JSON.stringify(history.slice(0, 2))}`);
});

test('a listener added later is called too', () => {
  const got = [];
  on((message) => got.push(message));
  const called = emit('ok');
  assert.equal(called, 3, `after adding a third listener, emit returned ${called}`);
  assert.deepEqual(got, ['ok'], `the new listener received ${JSON.stringify(got)}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

A counter that keeps its this

clickTwice takes increment off the counter and calls it on its own, so this is undefined and the tests stop with a TypeError. tsc does not notice, because increment does not declare this. Declare it in the Counter interface: increment(this: Counter): number. Run the tests: tsc now reports error TS2684 in clickTwice. Fix clickTwice so that it calls increment as a method of counter, twice, and returns the second result.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    this is the first parameter in the signature, and it is not a real parameter: increment(this: Counter): number;.

  2. Hint 2

    The object literal in makeCounter needs no change: it gets its this from the Counter return type.

  3. Hint 3

    Call the method with its object: counter.increment(); then return counter.increment();.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export interface Counter {
  count: number;
  step: number;
  increment(this: Counter): number;
}

export function makeCounter(step: number): Counter {
  return {
    count: 0,
    step,
    increment() {
      this.count += this.step;
      return this.count;
    },
  };
}

export function clickTwice(counter: Counter): number {
  counter.increment();
  return counter.increment();
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export interface Counter {
  count: number;
  step: number;
  increment(): number;
}

export function makeCounter(step: number): Counter {
  return {
    count: 0,
    step,
    increment() {
      this.count += this.step;
      return this.count;
    },
  };
}

export function clickTwice(counter: Counter): number {
  const click = counter.increment;
  click();
  return click();
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeCounter, clickTwice} from './main.ts';

test('increment adds the step', () => {
  const counter = makeCounter(2);
  const result = counter.increment();
  assert.equal(result, 2, `makeCounter(2).increment() returned ${result}`);
});

test('clickTwice returns the count after two clicks', () => {
  const result = clickTwice(makeCounter(5));
  assert.equal(result, 10, `clickTwice(makeCounter(5)) returned ${result}`);
});

test('clickTwice changes the counter it was given', () => {
  const counter = makeCounter(3);
  clickTwice(counter);
  assert.equal(counter.count, 6, `after clickTwice, count is ${counter.count}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

Returning a value from a function declared void

function save(name: string): void {
  console.log("saved " + name);
  return name.length;
}

What tsc or Node.js prints

main.ts(3,3): error TS2322: Type 'number' is not assignable to type 'void'.

Why, and the fix

A function whose own return type is void must not return a value; only a function stored in a void function type may. If callers need the length, say so: function save(name: string): number. If not, delete the return statement.

Using the result of a void callback

function runLater(f: () => void) {
  return f();
}
const r = runLater(() => 42);
console.log(r.toFixed());

What tsc or Node.js prints

main.ts(5,15): error TS2339: Property 'toFixed' does not exist on type 'void'.

Why, and the fix

f has the type () => void, so whatever it returns is treated as void, even though this arrow function returns 42. That is the point of void: the caller promises not to use the result. If runLater should pass the result on, type the callback with it: f: () => number, and give runLater the return type number.

An arrow function where this is declared

interface User {
  name: string;
  admin: boolean;
}

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

const users: User[] = [
  { name: "Ada", admin: true },
  { name: "Grace", admin: false },
];

const db: DB = {
  filterUsers(filter) {
    return users.filter((user) => filter.call(user));
  },
};

const admins = db.filterUsers(() => this.admin);

What tsc or Node.js prints

main.ts(21,37): error TS2532: Object is possibly 'undefined'.

Why, and the fix

An arrow function has no this of its own; it uses the this around it, and outside any function in main.ts there is none. filterUsers sets this for its callback only if the callback is a function expression. The Handbook: "you need to use function and not arrow functions". Write db.filterUsers(function (this: User) { return this.admin; }).

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

void: nothing useful comes back

The Handbook: "void represents the return value of functions which don't return a value." It is inferred for a function without return statements. A function declared : void must not return anything: return name.length in it is error TS2322: Type 'number' is not assignable to type 'void'. A function type is looser. () => void, as a callback type, accepts () => true; the value is ignored, and calling it gives void, so if (f1()) is error TS1345: An expression of type 'void' cannot be tested for truthiness. That is why src.forEach((el) => dst.push(el)) compiles: push returns a number, and forEach expects void. And void is not undefined: const x: undefined = log("hi") is error TS2322.

object, unknown and never, rather than Object, Function and any

object is any value that is not a primitive: objects, arrays and functions. function keys(o: object) rejects keys(42) with error TS2345. Object with a capital O accepts 42 too; the Handbook says "object is not Object. Always use object!" The type Function accepts any function, but a call through it returns any, so f(1, 2, 3).toUpperCase() compiles and fails at run time; () => void is safer. unknown, from B2.1, works in both directions: a parameter of type unknown accepts every value, and a return type of unknown, as in safeParse(s: string): unknown, makes callers check the value first. never, from B3.5, is for a function that never returns, such as fail(msg): never.

Declaring this

In an object literal, TypeScript infers this for a method: inside user.becomeAdmin, this is user. But a method taken off its object loses it: const greet = user.greet; greet() compiles and stops with TypeError: Cannot read properties of undefined (reading 'name'). JavaScript forbids a parameter named this, so TypeScript uses that place for this's type: increment(this: { count: number }). Callers pass nothing for it. Now const inc = counter.increment; inc() is error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type '{ count: number; }'. Callback APIs declare it too: filterUsers(filter: (this: User) => boolean). Pass a function expression there: an arrow function has no this of its own.

Sources

Last reviewed October 4, 2026