A union
A union type joins types with a bar. id: string | number means id can be a string or a number. Callers may pass either one, so show("ab") and show(7) are both allowed.
Without a check, id.toUpperCase() is an error. tsc allows only what works for every member of the union. Strings have toUpperCase, but numbers do not, so tsc refuses.
Narrowing
Line 2 asks a real question while the program runs: typeof id gives the text "string" or "number". tsc reads this if too, and uses it to work out what id can be on each path.
Inside the if, typeof id is "string", so id can only be a string. tsc narrows the union to string, and id.toUpperCase() on line 3 is now allowed.
Every string returns on line 3, so line 5 is reached only when id is not a string. tsc removes string from the union and number is left: id.toFixed(1) is allowed.
At run time
Now the program runs. show("ab") checks typeof "ab", which is "string", so it takes the if and returns "AB" from line 3.
show(7) checks typeof 7, which is "number", so it skips the if and returns "7.0" from line 5. console.log then prints both values: AB 7.0.
In show.js the types are gone, but the typeof check is still there. That is why narrowing uses real JavaScript checks such as typeof, ===, in and instanceof: they still work while the program runs.
What happens
;; step 1/8 · union
show.ts
> 1 function show(id: string | number) {
2 if (typeof id === "string") {
3 return id.toUpperCase();
4 }
5 return id.toFixed(1);
6 }
7 console.log(show("ab"), show(7));
id: string | number
either a string ("ab")
or a number (7)
Working out the type from ifs, returns and other checks is called control flow analysis. It also narrows after ===, in, instanceof and Array.isArray checks. The program is real and runnable: tsc 5.9 compiles it with no errors and node show.js prints AB 7.0.