Skip to content
aviral gupta

// B3.2 · ~30 min · Beginner

typeof, truthiness, equality and in

After this lesson you can narrow a union with typeof, a truthiness check, an equality check or in, and say which values each check lets through or drops.

Lesson 2 of 5 in B3 Narrowing

You will be able to

  • Narrow with typeof, knowing its eight result strings and that "object" still includes null and arrays
  • Use truthiness checks and ??, and spot where a truthiness check wrongly drops 0 or the empty string
  • Narrow with ===, == null, switch and the in operator, including optional properties
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: if (value) { … } skips its block for some values. For which of these does it skip the block? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: tsc reports no errors for this file. What does node main.ts print?

    function scoreLabel(score: number | null): string {
      if (score) {
        return "Score: " + score;
      }
      return "Not played yet";
    }
    
    console.log(scoreLabel(0));
  3. Practice · Activity 3 of 7

    Fill in the string, so that tsc accepts the function and the program prints 1.50 EUR.

    function format(value: string | number): string {
      if (typeof value === "____") {
        return value.toFixed(2);
      }
      return value.toUpperCase();
    }
    
    console.log(format(1.5), format("eur"));
    if (typeof value === "") {
  4. Practice · Activity 4 of 7

    v has the type Setting below. Match each check to the type v has inside its if block.

    type Setting = string | number | string[] | null | undefined;
  5. Practice · Activity 5 of 7

    Which line does tsc reject?

    function show(v: string | string[] | Date): string {
      if (typeof v === "object") {
        return v.join(", ");
      }
      return v.toUpperCase();
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. A human can swim or fly with the right equipment, so both are optional. Inside the if, what type does animal have?

    type Fish = { swim: () => void };
    type Bird = { fly: () => void };
    type Human = { swim?: () => void; fly?: () => void };
    
    function move(animal: Fish | Bird | Human) {
      if ("swim" in animal) {
        // what is animal here?
      }
    }
  7. Apply · Activity 7 of 7

    Mini-task. Write describeInput(value: Input) for type Input = string | number | string[] | null | undefined. It returns "nothing" for null and undefined, "N items" for an array, "the number N" for a number, "empty text" for "" and "text …" for any other string. Make sure 0 and "" get their own answers, then print four calls with console.log. 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

A settings report

show turns every kind of setting into text. == null removes null and undefined in one check, then typeof picks string, number and boolean, and only string[] is left for join. Note the order: 0, false and "" reach their own branches because the first check is == null, not a truthiness check. connection uses in to tell a wired device from a wireless one; password is optional, so it still needs a check. Run npx tsc, then node main.ts. Then change the first line of show to if (!value) and compare the output.

main.ts

// A settings report. Each check narrows value for the code it guards.
type Setting = string | number | boolean | string[] | null | undefined;

function show(value: Setting): string {
  if (value == null) return "(not set)"; // == null removes null and undefined
  if (typeof value === "string") return '"' + value + '"';
  if (typeof value === "number") return value.toFixed(1);
  if (typeof value === "boolean") return value ? "on" : "off";
  return value.join(", "); // only string[] is left
}

type Wired = { port: number; cable: string };
type Wireless = { port: number; network: string; password?: string };

function connection(device: Wired | Wireless): string {
  if ("network" in device) {
    // here device is Wireless; password may still be missing
    return "wifi " + device.network + (device.password ? " (locked)" : " (open)");
  }
  return "cable " + device.cable; // here device is Wired
}

const settings: Setting[] = ["dark", 0, false, ["de", "en"], "", null, undefined];
for (const value of settings) {
  console.log(show(value));
}
console.log(connection({ port: 1, cable: "LAN" }));
console.log(connection({ port: 2, network: "home", password: "secret" }));
console.log(connection({ port: 3, network: "cafe" }));

Run it with

npx tsc
node main.ts

Output

"dark"
0.0
off
de, en
""
(not set)
(not set)
cable LAN
wifi home (locked)
wifi cafe (open)
  • With if (!value) as the first check, 0, false and "" would all print (not set), and tsc would still report no errors.
  • After the three typeof checks return, value can only be a string[], so value.join compiles without a check.
  • In the true branch of "network" in device, device is Wireless; in the rest of the function it is Wired.
  • device.password is optional, so the in check does not make it defined; the ? : expression checks it.
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

Keep the zero

stockLabel should say "unknown" only when the count is null or undefined, and startVolume should fall back to 50 only when nothing was saved. Both use a truthiness check, so a count of 0 and a muted volume of 0 come out wrong. tsc reports nothing: the types are fine. Fix the two checks so the tests pass.

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 values make !count true? 0 is one of them, but 0 is a real count.

  2. Hint 2

    count == null is true for exactly null and undefined, and nothing else.

  3. Hint 3

    saved || 50 falls back for every falsy value. The operator that falls back only for null and undefined is ??.

Show a solution

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

export function stockLabel(count: number | null | undefined): string {
  if (count == null) {
    return "unknown";
  }
  return count + " in stock";
}

export function startVolume(saved: number | undefined): number {
  return saved ?? 50;
}
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 function stockLabel(count: number | null | undefined): string {
  if (!count) {
    return "unknown";
  }
  return count + " in stock";
}

export function startVolume(saved: number | undefined): number {
  return saved || 50;
}

main.test.ts

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

test('stockLabel(5) is "5 in stock"', () => {
  assert.equal(stockLabel(5), '5 in stock', 'stockLabel(5) should be "5 in stock"');
});

test('stockLabel(0) is "0 in stock"', () => {
  assert.equal(stockLabel(0), '0 in stock', 'stockLabel(0) should be "0 in stock": 0 is a real count');
});

test('stockLabel(null) and stockLabel(undefined) are "unknown"', () => {
  assert.equal(stockLabel(null), 'unknown', 'stockLabel(null) should be "unknown"');
  assert.equal(stockLabel(undefined), 'unknown', 'stockLabel(undefined) should be "unknown"');
});

test('startVolume(undefined) is 50', () => {
  assert.equal(startVolume(undefined), 50, 'startVolume(undefined) should fall back to 50');
});

test('startVolume(0) is 0', () => {
  assert.equal(startVolume(0), 0, 'startVolume(0) should stay 0: a muted volume was saved');
});

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

Reach a contact

A Contact has either an email or a phone, and a phone may be stored as text or as a number. tsc reports error TS2339: Property 'email' does not exist on type 'Contact'. Make reach return "write to <email>" for an email, "call <phone>" for a phone stored as text, and "call +<phone>" for a phone stored as a 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

    Only Email declares email, so contact.email is not allowed on a Contact. "email" in contact narrows contact to Email.

  2. Hint 2

    After that if returns, contact is a Phone. typeof contact.phone === "number" then narrows the property.

  3. Hint 3

    Three returns: "write to " + contact.email, "call +" + contact.phone for a number, and "call " + contact.phone for text.

Show a solution

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

export type Email = { email: string };
export type Phone = { phone: string | number };
export type Contact = Email | Phone;

export function reach(contact: Contact): string {
  if ("email" in contact) {
    return "write to " + contact.email;
  }
  if (typeof contact.phone === "number") {
    return "call +" + contact.phone;
  }
  return "call " + contact.phone;
}
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 Email = { email: string };
export type Phone = { phone: string | number };
export type Contact = Email | Phone;

export function reach(contact: Contact): string {
  return "write to " + contact.email;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {reach} from './main.ts';

test('an email contact gives "write to …"', () => {
  assert.equal(reach({email: 'ada@example.com'}), 'write to ada@example.com', 'reach({email: "ada@example.com"}) should be "write to ada@example.com"');
});

test('a phone stored as text gives "call …"', () => {
  assert.equal(reach({phone: '+49 30 1234567'}), 'call +49 30 1234567', 'reach({phone: "+49 30 1234567"}) should be "call +49 30 1234567"');
});

test('a phone stored as a number gives "call +…"', () => {
  assert.equal(reach({phone: 49301234567}), 'call +49301234567', 'reach({phone: 49301234567}) should be "call +49301234567"');
});

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

Using in on a value that may be a string

type Tags = string | string[];

function count(tags: Tags): number {
  if ("length" in tags) {
    return tags.length;
  }
  return 1;
}

console.log(count(["a", "b"]));

What tsc or Node.js prints

main.ts(4,19): error TS2322: Type 'Tags' is not assignable to type 'object'.

Why, and the fix

in only works on objects. tags may be a string, and at run time count("news") would stop with TypeError: Cannot use 'in' operator to search for 'length' in news. Since TypeScript 4.9, tsc checks that the right side of in is an object. Pick the members with a check that works on every value: if (Array.isArray(tags)) for the array, or typeof tags === "string" for the text.

Removing undefined but forgetting null

function initial(name: string | null | undefined): string {
  if (name !== undefined) {
    return name.charAt(0);
  }
  return "?";
}

console.log(initial("Ada"));

What tsc or Node.js prints

main.ts(3,12): error TS18047: 'name' is possibly 'null'.

Why, and the fix

!== undefined removes only undefined; null is still possible inside the if. Use the loose check name != null, which removes null and undefined together, or name !== null && name !== undefined. A truthiness check, if (name), would also compile, but it sends the empty string to "?" as well; that is fine only if that is what you want.

Calling an optional method after an in check

type Fish = { swim: () => void };
type Human = { name: string; swim?: () => void };

function dive(animal: Fish | Human) {
  if ("swim" in animal) {
    animal.swim();
  }
}

dive({ swim: () => console.log("splash") });

What tsc or Node.js prints

main.ts(6,5): error TS2722: Cannot invoke an object which is possibly 'undefined'.

Why, and the fix

Human declares swim as optional, so it stays in the true branch of the in check, and there swim may be undefined. The in check tells you the property is declared, not that it holds a function. Check the value itself: if (animal.swim) { animal.swim(); }, or call it with animal.swim?.().

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

typeof: eight answers, and what "object" still lets through

typeof returns one of eight strings: "string", "number", "bigint", "boolean", "symbol", "undefined", "object" and "function". Comparing it with one of them is a type guard: inside if (typeof v === "number"), v is a number, and the else branch gets the other members. typeof never returns "null" or "array": null and arrays both give "object". So for v: string | string[] | Date | null, typeof v === "object" leaves string[] | Date | null. Add v !== null to remove null, and use Array.isArray(v) to pick the arrays. typeof v === "function" picks the function members. A misspelt string such as "strng" is error TS2367: typeof can never return it.

Truthiness: an if drops more than null

An if accepts any value, not only booleans. It treats false, 0, NaN, "", 0n, null and undefined as false and everything else as true, including "0", " " and []. TypeScript narrows with this: inside if (count), count: number | undefined is a number. The other branch keeps number | undefined, because the number may be 0. That is the trap: if (!count) return "unknown" also answers "unknown" for 0, and tsc says nothing, because the code is type-correct; only the logic is wrong. When 0 or "" are real values, test what you mean: count == null, count !== undefined, or count ?? 50 instead of count || 50. ?? falls back only for null and undefined.

Equality and in

=== and !== against a value narrow too: after if (v === null) return, null is gone. === undefined removes only undefined; the loose == null and != null remove null and undefined together, the one common use of ==. A switch narrows each case the same way. "network" in device checks whether the object has that property: the true branch keeps the members that declare it, required or optional, and the false branch the members where it is optional or missing. So an optional property appears on both sides, and inside the true branch it may still be undefined. The right side of in must be an object, or tsc reports error TS2322.

Sources

Last reviewed October 4, 2026