Course outline

TypeScript / Basic / Lesson 2

Values, inference, and control flow

Use inference to express intent without redundant annotations, then reason about JavaScript values and boundary conditions explicitly.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish primitive types, literal types, and widening.
  • Explain number limitations and explicit domain checks.
  • Write branches and loops with defined boundary behavior.

Inference follows expressions and context

TypeScript infers types from initializers and surrounding context. let score = 3 generally permits later numeric assignments, while const mode = "ready" can retain a narrower literal type because the binding cannot be reassigned. const does not recursively freeze an object or array; properties can still mutate unless their interface restricts that access. Use annotations where they communicate a boundary or prevent accidental widening, rather than repeating every obvious initializer type. Inference is a static convenience and does not change how JavaScript allocates or compares values.

Number is not an integer-domain guarantee

The number type includes fractional values, NaN, and infinities. It does not mean a finite nonnegative integer or an exact money amount. JavaScript uses floating-point number arithmetic, so very large integers and decimal fractions need an explicit representation and rounding policy. Number.isSafeInteger and range checks can enforce a chosen count domain at runtime. bigint represents integers with different operations and serialization constraints; it should not be mixed casually with number. The first example checks validity before accepting counts, demonstrating that a type annotation alone cannot express or enforce every numeric business rule.

typescript A numeric type with additional runtime constraints
export {};
function validCount(value: number): boolean {
  return Number.isSafeInteger(value) && value >= 0 && value <= 100;
}
console.log(validCount(12), validCount(2.5), validCount(Infinity));
Expected output
true false false

Twelve satisfies every rule. A fraction and infinity are numbers but fail the accepted count domain.

Assign every boundary deliberately

if and conditional expressions choose behavior from actual runtime conditions. JavaScript truthiness is available, but explicit comparisons often better describe numeric and text requirements. Testing if count conflates zero with absence, and testing if text conflates an empty string with a missing value. Distinguish null and undefined from legitimate empty data where the contract requires it. Use strict equality when coercion is not intended. TypeScript can narrow values after conditions, but a logically wrong threshold remains wrong even when every expression type-checks. Test equality at the threshold as well as its neighbors.

A loop needs progress and a state explanation

A loop should have initialization, a continuation condition, and a progress step that eventually reaches an exit for accepted input. The second example sums one through four. Before each iteration, total equals the sum of positive integers smaller than next; adding next preserves that statement. When next becomes five, the condition fails and total is ten. This invariant explains the result independently of running the code. User-controlled loop bounds need resource limits and numeric validation, because a number annotation alone does not prevent an enormous or infinite computation.

typescript A finite sum with an inclusive threshold
export {};
let total = 0;
for (let next = 1; next <= 4; next += 1) { total += next; }
const state = total >= 10 ? "ready" : "wait";
console.log(total, state);
Expected output
10 ready

Four additions produce ten, and equality selects ready. Changing <= to < changes the visited set.

Use the representation the task actually needs

string.length measures UTF-16 code units, not a universal count of user-perceived characters. Array.from over a string iterates Unicode code points, while grapheme clusters require another explicit text policy. boolean represents two logical values and does not encode an authorization decision by itself. Avoid using primitive wrapper types such as String in ordinary parameter annotations when string is intended. A compiler checks assignability within its model; domain units, display length, financial rounding, and iteration limits still require requirements and tests. Clear variable names and small pure functions make those obligations easier to review.

Common errors and fixes

Zero is treated as missing data.

Why it happens
A truthiness condition replaced an absence check.
How to fix it
Compare explicitly against null or undefined as required.
Prevent it next time
Test legitimate empty and zero values.

A count accepts fractions or infinity.

Why it happens
number was assumed to mean a bounded integer.
How to fix it
Add safe-integer and range validation.
Prevent it next time
Write numeric domain rules beside the interface.

A const object still changes.

Why it happens
Binding immutability was mistaken for deep freezing.
How to fix it
Design readonly access or deliberate runtime freezing where needed.
Prevent it next time
Separate aliasing and mutation policy from const syntax.

Check your understanding

1. Predict the sum example with next < 4 instead of next <= 4. Explain the changed output using the invariant.

Show worked solution

The loop visits one, two, and three, so it prints 6 wait. The invariant remains valid: total is the sum of values already visited. The range changed, not the addition operation. This is why a test should cover the intended final element and the exact threshold, rather than merely confirming that the loop terminates.

2. Design acceptance rules for a page size that must be an integer from one through fifty. Classify 0, 1, 2.5, 50, and 51.

Show worked solution

Require Number.isSafeInteger(value), value >= 1, and value <= 50. Only one and fifty are accepted from this list. Zero and fifty-one violate bounds, and two and a half violates integrality. Keep the checks at runtime for external values; declaring a parameter number does not perform those tests or convert malformed input.

Key takeaways

  • Inference reduces repetition without changing runtime semantics.
  • Primitive types still need domain validation.
  • Explain loops and test boundary equality explicitly.

Official references