Before you begin
Why this matters
Most values in your code get their type without you writing it, and some values change type without you asking. Knowing which type the compiler chose, and where JavaScript converts on its own, lets you predict results that otherwise look random.
What this helps you reason about
- Predict the type TypeScript gives a name declared with let and with const.
- Spot a place where JavaScript converts a value on its own, such as sort with no compare function.
- Choose between Number and parseInt, and know what each one loses.
A library program works with page counts, room names, and text typed at a desk. This lesson asks how TypeScript decides the type of each value, when JavaScript converts a value on its own, and what a conversion you write can lose.
Already know this? The short version
- let x = 5 infers number, but const x = 5 infers the type 5, a type with one value; properties of an object literal widen to string and number unless you write as const.
- TypeScript refuses most mixed operations ("5" * 2, "5" == 5), but JavaScript still converts on its own where the types allow it: + with a string, template literals, and sort() with no compare function all turn values into text, and a value of type any lets every such conversion through.
- Number(text) converts the whole text or gives NaN (and "" gives 0); parseInt(text) keeps the leading digits and drops the rest silently, so "2.5" and "2,5" both give 2.
- After converting, check: Number.isInteger, Number.isNaN, and a range test, once, where the text enters.
Words in this lesson (5)
- type
- The kind of a value, such as number, string, or boolean. It decides which operations TypeScript allows on the value.
- type inference
- The compiler working out the type of a name or an expression from the code, without the type being written.
- literal type
- A type with exactly one value, such as "reading" or 5. TypeScript infers one for a const declared with a string or a number.
- coercion
- A conversion the language makes on its own, without any conversion written in the code. Also called an implicit conversion.
- explicit conversion
- A conversion written in the code, such as Number(text) or parseInt(text). It converts the value it is given and nothing else.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Inference
How does the compiler work out a type?
As Variables showed, TypeScript works out the type of a name from its first value. This is called type inference: the compiler works out a type from the code, without the type being written.
The declaration decides how wide that type is. let room = "reading" gives room the type string, because a let can later hold any other string. A const can never change, so const room = "reading" gets a narrower type: the literal type "reading", a type with exactly one value.
In the program below, type Room = "reading" | "archive" names a type whose only values are those two strings; | means “or”. openRoom accepts room, because its type is "reading". If room were declared with let, the compiler would refuse the call: Argument of type 'string' is not assignable to parameter of type 'Room'.
type Room = "reading" | "archive";
function openRoom(room: Room): string {
return "Opening the " + room + " room";
}
const room = "reading"; // type "reading"
console.log(openRoom(room)); // Opening the reading room TypeScript infers a type from the first value; const keeps the exact literal type, and let widens it to string or number.
Coming from Python: Python decides types only while the program runs; TypeScript infers them before the run, checks them, and then removes them.
Precisely: object properties widen
Inside an object, properties widen even with const: const visit = { room: "reading" } gives visit.room the type string, because a property can be changed later. as const keeps the literal types: { room: "reading" } as const.
Experiment
Pass a const and a let to the same function
A const never changes, so TypeScript infers the literal type "reading" for it, and "reading" is one of the two values Room allows. A let can be given any other string later, so TypeScript infers string, and a string might not be a Room. On the stepper, <top level> is the name for the code outside every function, and Step sometimes stops on the same line twice; that is normal for this debugger.
Why it matters The inferred type depends on the declaration as well as the value. When a call is refused, ask which type the compiler inferred, not only which value the name holds.
Predict whether openRoom accepts room. Then change const room = "reading"; to let room = "reading"; and predict whether any line runs.
type Room = "reading" | "archive";
function openRoom(room: Room): string {
return "Opening the " + room + " room";
}
const room = "reading";
console.log(openRoom(room));
What to look for
Step into openRoom: its frame shows room "reading", and the run prints Opening the reading room. The panel shows values, not types, so room looks the same with const and with let. After the edit, nothing runs; the compiler reports Line 8: Argument of type 'string' is not assignable to parameter of type 'Room'.
Expected output
Opening the reading room02 / Coercion
When does the language convert a value for you?
A coercion is a conversion the language makes on its own, without any conversion written in the code. TypeScript refuses most of them before the run. "5" * 2 is a compile error, and so is "5" == 5: This comparison appears to be unintentional because the types 'string' and 'number' have no overlap.
JavaScript still coerces in the places TypeScript allows. What is a value? showed + joining a string and a number. A template literal, text between backquotes such as `${copies} copies`, turns each value inside ${…} into text. And sort() with no compare function turns every element into text and compares the texts by character code, so 1040 comes before 95. sort also changes the array itself, in place, and returns that same array. toSorted() returns a sorted copy and leaves the original as it was; use it where other code still needs the original, such as an array kept in React state.
In the code below, (a, b) => a - b is an arrow function: a short way to write a function with parameters a and b that returns a - b. sort calls it to compare two elements as numbers. join(" ") makes one string of the elements, with a space between them.
const pages = [120, 95, 1040];
pages.sort();
console.log(pages.join(" ")); // 1040 120 95
pages.sort((a, b) => a - b);
console.log(pages.join(" ")); // 95 120 1040 TypeScript refuses most coercions, but where JavaScript turns a value into text on its own, as in sort(), the compiler says nothing.
Coming from Python: sorted([120, 95, 1040]) compares numbers as numbers; JavaScript’s sort() with no compare function compares them as text.
Precisely: any lets every coercion through
A value of type any, such as the result of JSON.parse, switches the compiler’s check off, so JavaScript’s full rules apply: "5" * 2 is 10, and "5" == 5 is true, because == converts the string to a number before it compares. === never converts.
Experiment
Sort numbers with no compare function
With no compare function, sort turns every element into text and compares the texts character by character: "1040" comes before "120" because "0" comes before "2". The compare function returns a negative number when a should come first, so sort compares the numbers as numbers.
Why it matters A conversion to text can hide inside a library call. Give sort a compare function whenever the elements are numbers.
Predict the order sort() leaves pages in. Then change pages.sort(); to pages.sort((a, b) => a - b); and predict again.
const pages = [120, 95, 1040];
pages.sort();
console.log(pages.join(" "));
What to look for
Step past line 2. pages shows [120, 95, 1040], then [1040, 120, 95], and the run prints 1040 120 95. After the edit, the stepper stops in arrow function (line 2) each time sort compares two pages, with a and b in its frame, such as a 95 and b 120. pages keeps showing the old order until sort has finished, then shows [95, 120, 1040], and the run prints 95 120 1040.
Expected output
1040 120 9503 / Conversion
What can an explicit conversion lose?
An explicit conversion is one written in the code. Number(text) and parseInt(text) both turn text into a number, and they lose different things.
Number converts the whole text. When any part of it is not a number, the result is NaN, “not a number”. Empty text is the exception: Number("") is 0, not NaN, and so is text with only spaces. So an empty form field becomes a valid-looking 0. parseInt reads digits from the start and stops at the first character that is not part of a whole number, without any sign that it stopped: parseInt("2.5") is 2, and parseInt("2 hours") is 2. Math.trunc(x) cuts the fraction off a number; it does not round.
console.log(Number("2.5")); // 2.5
console.log(parseInt("2.5")); // 2
console.log(Number("2,5")); // NaN
console.log(parseInt("2,5")); // 2
console.log(Math.trunc(2.9)); // 2 Number converts all of the text or gives NaN; parseInt keeps the leading digits and drops the rest without a sign.
Precisely: more of Number’s rules
Number(" 12 ") is 12, because spaces around the number are ignored, and Number("0x10") is 16. parseInt("0x10") is 16 as well; parseInt("0x10", 10), with the base given, reads only the 0. Both follow the ECMAScript rules, so every engine gives the same results.
Experiment
Convert typed hours with parseInt and with Number
parseInt reads digits from the start and stops at the first other character, the point or the comma, without any sign that it stopped. Number converts the whole text, and gives NaN when any part of it is not a number.
Why it matters parseInt drops the rest of the text silently; Number shows, with NaN, that the text was not a number. Choose the conversion whose loss you can see.
Predict whole and exact. Then change "2.5" to "2,5", with a comma, and predict again.
const typed = "2.5";
const whole = parseInt(typed);
const exact = Number(typed);
console.log(whole, exact);
What to look for
Step past each line: typed shows "2.5", whole shows 2, and exact shows 2.5. The run prints 2 2.5. After the edit, whole still shows 2, exact shows NaN, and the run prints 2 NaN.
Expected output
2 2.504 / Safe conversion
How do you convert without losing a value?
A conversion that can lose part of a value needs a check. For text, convert with Number, then test the result. Number.isInteger(x) is true when x is a whole number, and false for NaN. Then test the range the program needs. Where 0 is a valid value, test for empty text before converting, because Number("") is 0.
As Static and dynamic type checks showed, the check belongs where the text enters the program. After it, the rest of the code works with a number it can trust.
function readCopies(text: string): number | null {
const copies = Number(text);
if (!Number.isInteger(copies) || copies < 1 || copies > 20) {
return null;
}
return copies;
}
// readCopies("3") is 3; readCopies("2.5") and readCopies("") are null Convert, then check that the result is the kind and size of number you need, before anything uses it.
Precisely: safe integers
Number.isSafeInteger(x) also checks that a whole number is at most 2^53 - 1, so that it is stored exactly. Number.isNaN(x) tests for NaN alone.
05 / Try it
Can you explain a fresh example?
Decide where each value is converted, and what that conversion keeps. Assertions, tests, and counterexamples looks for inputs, such as a value just past a range, that break code like this.
const typed = "12.9";
const pages = [9, 30, 200];
pages.sort();
console.log(parseInt(typed), Number(typed), pages.join(" "));
What is printed?
Reveal the reasoning
12 12.9 200 30 9. parseInt keeps the digits before the point. Number converts all of the text. sort() with no compare function turns each page count into text, and "200" comes first because the character "2" has a smaller code than "3" and "9".
Practice tickets
- CONV-101 · The average number of visitors never has a fraction → Free
- CONV-102 · Visitors at exhibit 300 hear the recording for exhibit 44 → Sign in · Pro
- CONV-103 · Suggested catalogue numbers lose their leading zeros → Sign in · Pro
Keep exploring
Assertions, tests, and counterexamples →