Start learning free

LESSON 87 OF 348 · CONDITIONAL STATEMENTS

Union Types in TypeScript: Narrowing with typeof

Lesson Read the full explanation

So far every variable had exactly one type: a string, a number, a boolean. But real programs are messier — a user ID might arrive as "abc-123" or as 42. TypeScript handles this with a union type: write the allowed types separated by | (read it as "or").

let id: string | number = "abc-123";
id = 42;      // fine — number is part of the union
id = true;    // error — boolean is NOT in the union

A string | number variable can hold either kind of value, and TypeScript tracks that for you.


The trap:

Here is where beginners get stuck. While a value's type is still the whole union, you may only use operations that are safe for every member. Strings have .length, numbers don't — so this refuses to compile:

function describe(x: string | number): string {
    return "length is " + x.length;   // numbers have no .length!
}
error TS2339: Property 'length' does not exist on type 'string | number'.
  Property 'length' does not exist on type 'number'.

Read the second line carefully — the compiler even names the guilty member: it's number that has no .length. You WILL see this error; it means "prove which type you have first".


Narrowing with typeof:

The proof is an ordinary if. The typeof operator returns the runtime type as a string ("string", "number", ...), and TypeScript understands the check: inside the branch, the union narrows to a single type.

if (typeof x === "string") {
    // here x is a string — .length is allowed
} else {
    // only number is left — x * 2 is allowed
}

Notice the else: no second check needed. Ruling out string leaves exactly one member, so TypeScript narrows x to number automatically.


Assignment
Follow the Coding Tutorial and write a function describe(x) where x is a string | number, returning a string:

  • if x is a string, return "string of length " plus x.length,
  • otherwise, return "number doubled to " plus x * 2.

Then call it with "Algo" and with 21, printing each result with console.log, so both branches run. The exact expected output is:

string of length 4
number doubled to 42

Hint
Compare against the string "string": typeof x === "string". Writing typeof x === string (no quotes) is itself a type error.


STEP 1 OF 6
    Ctrl + Enter to check

    TYPESCRIPT · CONDITIONAL STATEMENTS

    Union Types in TypeScript, explained

    What a union type is

    Most variables hold one kind of value, but some data legitimately comes in more than one form: an ID might be the string "abc-123" or the number 42. A union type lists the allowed types separated by |, which reads as "or":

    let id: string | number = "abc-123";
    id = 42;      // fine: number is part of the union
    id = true;    // error: boolean is not in the union
    

    The compiler reports the last line as Type 'boolean' is not assignable to type 'string | number'. Unions are checked at compile time only. They are erased from the JavaScript that tsc writes, so nothing stops a wrongly typed value from arriving at run time, for example from parsed JSON. Even with the error above, tsc still writes the JavaScript, and the program runs and holds true in id.

    The trap: only shared operations are allowed

    While a variable's type is the whole union, you can use only operations that are valid for every member. Strings have .length and numbers do not:

    function describe(x: string | number): string {
        return "length is " + x.length;
    }
    
    error TS2339: Property 'length' does not exist on type 'string | number'.
      Property 'length' does not exist on type 'number'.
    

    The second line names the member that causes the problem. Read it as "prove which type you have first". If you ignore the error and run the JavaScript anyway, describe(42) gives "length is undefined". That is the bug the compiler was protecting you from.

    Narrowing with typeof

    The proof is an ordinary if. The typeof operator returns the run-time type of a value as a string, such as "string", "number" or "boolean". TypeScript understands checks of the form typeof x === "string" and narrows the type of x inside the branch:

    if (typeof x === "string") {
        // here x is a string, so .length is allowed
    } else {
        // only number is left, so x * 2 is allowed
    }
    

    The else needs no second check. Once string is ruled out, one member remains, and TypeScript narrows x to number by itself. With three members you can chain checks, and the last else gets whatever is left:

    function f(x: string | number | boolean) {
        if (typeof x === "string") return "s";
        else if (typeof x === "number") return "n";
        else return x ? "yes" : "no";
    }
    console.log(f("a"), f(1), f(true));
    
    s n yes
    

    Gotchas

    • Compare with a string. Write typeof x === "string", with quotes. Without them, string is a type name, not a value, and the compiler reports an error.
    • Narrowing is not conversion. It only tells the compiler what you have proved. It never changes the value. Likewise, a cast such as x as string checks nothing at run time.
    • typeof is coarse. For null and for arrays it returns "object", so it can't tell those two apart. For unions of strings, numbers and booleans it is exactly the right tool.
    • Narrowing lasts within the branch. After the if/else ends, the variable has its full union type again unless every branch has returned.
    • Methods shared by all members are fine. Both strings and numbers have toString(), so x.toString() compiles on a string | number without narrowing.

    The lesson's exercise

    The task is to write describe(x), where x is a string | number, returning a string: the text "string of length " plus the length for a string, and "number doubled to " plus the doubled value for a number. Then call it with "Algo" and with 21 and print both results.

    1. Declare the function with the parameter typed string | number and the return type string. The union is what lets one function accept both kinds of argument.
    2. Open an if that tests typeof x === "string". Inside it, x is a string.
    3. Return the string message using x.length. This compiled only because of the narrowing. The + operator joins the text and the number, converting the number to text.
    4. Add an else that returns "number doubled to " followed by x * 2. The multiplication is legal because only number remains.
    5. Call and print. Pass "Algo" to describe and log the result, then do the same with 21, so both branches run.
    function describe(x: string | number): string {
        if (typeof x === "string") {
            return "string of length " + x.length;
        } else {
            return "number doubled to " + x * 2;
        }
    }
    console.log(describe("Algo"));
    console.log(describe(21));
    

    The output is:

    string of length 4
    number doubled to 42
    

    In the second return, * binds tighter than +, so x * 2 is computed first and the result is joined to the text.

    Key takeaways

    • A union type A | B allows a value of either type, and the check happens at compile time only.
    • On a union you may use only operations that are valid for every member.
    • An if with typeof narrows the union inside each branch, and the else narrows to whatever remains.
    • Narrowing changes what the compiler knows, not the value itself.
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free