Narrowing with typeof

A function that gets a string | number usually needs to treat each type differently. In JavaScript, you find out the type with typeof, which you know from the JavaScript course: typeof 50 is "number", and typeof "Silver Sword" is "string". TypeScript understands such a check. In this example, the guild pays out quest rewards, and it keeps 5 gold of every gold reward for itself:

function payout(reward: string | number): string {
    if (typeof reward === "string") {
        return `You get the ${reward.toUpperCase()}`;
    } else {
        return `You get ${reward - 5} gold (5 for the guild)`;
    }
}

console.log(payout("Silver Sword"));
console.log(payout(50));

This prints:

You get the SILVER SWORD
You get 45 gold (5 for the guild)

Inside the if, TypeScript knows that reward is a string, so toUpperCase() is allowed. In the else branch, only number is left, so subtraction is allowed. This is called narrowing: a check makes the type narrower inside a branch, from string | number to just string, or just number.

Without the check, TypeScript doesn't allow the subtraction:

function payout(reward: string | number): string {
    return `You get ${reward - 5} gold (5 for the guild)`;
}
main.ts(2,23): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

You know this message from the Basic Types lesson: the value on the left of - must be a number.

Narrowing also works after return. When the if branch ends with return, the rest of the function runs only for the other type, and TypeScript knows that:

function payout(reward: string | number): string {
    if (typeof reward === "string") {
        return `You get the ${reward.toUpperCase()}`;
    }
    return `You get ${reward - 5} gold (5 for the guild)`;
}

Below the if, reward is a number. Programmers often write functions this way: first the special case, which returns, then the main case without extra braces.

Arrays can hold unions too. TypeScript infers the type of [12, "Old Key", 40] as (string | number)[]: an array whose elements are texts or numbers. When you write such a type yourself, don't forget the parentheses. Without them, number | string[] means “a number, or an array of texts”, and TypeScript refuses the numbers in the array:

const loot: number | string[] = [12, "Old Key", 40];
main.ts(1,34): error TS2322: Type 'number' is not assignable to type 'string'.
main.ts(1,49): error TS2322: Type 'number' is not assignable to type 'string'.

Typical mistakes:

  • typeof gives the name of the type in lowercase: "string", "number", "boolean". For typeof reward === "String", TypeScript reports error TS2367: This comparison appears to be unintentional because the types '"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"' and '"String"' have no overlap. The long list in the message shows every text that typeof can give, and "String" isn't one of them.
  • Narrowing applies only inside the branch, or after an if that returns. Below an if that doesn't return, the type is string | number again.

Instructions

Run the program: TypeScript doesn't allow price * count, because price may be a text. At the beginning of formatPrice, add a typeof check: when price is a text, the function returns it in capital letters. The program then prints:

50 gold
FREE

Below the calls, create an array prices of the type (number | string)[] with the values 12, "sold out" and 40.

Go through prices with for...of, and print formatPrice(price, 2) for each price. The program then prints:

50 gold
FREE
24 gold
SOLD OUT
80 gold

Start programming for free

By signing up, you agree to the Terms of Service and Privacy Policy.

Or sign up with:

2/10