2 Basic Types
This chapter explains how primitive types give meaning to bits and restrict possible values, then uses TypeScript to illustrate how these types compose into reliable, efficient programs. It surveys foundational primitives—empty and unit types, booleans, numbers, strings, arrays, and references—and emphasizes that understanding their semantics is essential both for correctness (avoiding subtle bugs) and for performance (choosing data representations that match access patterns and data shape).
It distinguishes non-terminating functions that should return the empty type (never) from procedures that complete but yield no meaningful result (void). For booleans, it highlights short-circuit evaluation and the practical tip to order conditions from cheaper to more expensive checks. The discussion of numbers covers width and encoding (unsigned, two’s complement, IEEE 754), overflow/underflow strategies (wrap, saturate, error) and how to detect them, and the realities of floating-point rounding—why 0.1 + 0.1 + 0.1 may not equal 0.3—along with remedies such as representing currency with safe integers and comparing floats using an epsilon threshold instead of direct equality.
On text, it shows why strings are tricky: users see graphemes, while programs manipulate code points encoded as bytes (UTF-8/16/32). Naive character- or byte-level slicing can split a single visible symbol (like an emoji) into invalid pieces, so the chapter recommends relying on libraries that operate at grapheme boundaries. Finally, it frames arrays and references as the building blocks of data structures: fixed-size arrays provide fast random access but are expensive to resize; reference-based structures (linked lists, tree nodes) excel at sparse or append-heavy scenarios. It contrasts array- and reference-backed implementations for lists and binary trees and explains how associative arrays (hash tables) can be built from arrays plus lists, concluding with guidance to choose representations based on expected read/write patterns and data density.
AND, OR, and NOT truth tables
4-bit unsigned integer encoding. Smallest possible value, when all 4 bits are 0, is 0. Largest possible value, when all bits are 1, is 15 (1 x 23 + 1 x 22 + 1 x 21 + 1 x 20).
4-bit signed integer encoding. -8 is encoded as 24 – 8 (1000 binary), -3 is encoded as 24 – 3 (1101 binary). The first bit is always 1 for negative numbers, 0 for positive numbers.
Different ways to handle arithmetic overflow: an odometer wraps around from 999999 back to 0; a dial knob simply stops at the maximum possible value; a pocket calculator prints “Error” and stops.
Floating-point representation of 0.10. In-memory binary representation of the 3 components: sign bit, exponent, and mantissa, the formula to convert the binary representation to a number, and the result of applying the formula. 0.10 is approximated to 0.100000000000000005551115123126.
Character encoding of the woman police officer emoji (police officer emoji character + zero-width joiner + female sign emoji) and resulting grapheme (woman police officer).
Woman police officer emoji viewed as UTF-16 string encoding of bits in memory, UTF-16 byte sequence, sequence of Unicode code points, and grapheme
5 32-bit integers stored in a fixed-size array and in a linked list. Finding an element is extremely fast in the fixed-size array as we can compute its exact location. On the other hand, a linked list requires us to follow the “next” elements until we find the element we are looking for. Elements can be anywhere in memory.
An array-based list with 9 elements and capacity for 16. 7 more elements can be appended before the data has to be moved to a larger array.
Binary tree represented as a fixed-size array. The missing node (right child of 2) is an unused element in the array. The parent-child relation between the nodes is implicit, as the index of a child can be computed from the index of the parent and vice-versa.
A sparse binary tree with only 3 nodes still requires an array with 7 elements to be represented correctly. If node 9 had a child, the array size would become 15.
Sparse tree represented using references. The diagram on the right represents the node data structure as value, left reference, right reference.
Associative array implemented as an array of lists. This instance contains the key-value mappings 0 à 10, 2 à 9, 5 à 10, 42 à 0.
FAQ
What is the empty type (never) and when should I use it?
The empty type is a type with no possible values. In TypeScript it’s written asnever and is ideal for functions that cannot return: they always throw, loop forever, or terminate the process. Declaring a function as (): never communicates intent and lets the compiler verify that no code path returns, preventing accidental changes that would make the function return.How is the unit type (void) different from the empty type (never)?
void means “returns, but no meaningful value,” while never means “does not return at all.” Use void for side-effect-only functions that complete (for example, console.log), and never for functions that cannot complete normally (always throw, crash, or run forever). Treating these as distinct improves readability and enables better compiler checks.Can I implement empty and unit types myself if a language doesn’t provide them?
Yes. An empty type can be modeled as an enum with no cases or a class/struct with an uncallable constructor (for example, private) so no instance can exist. A unit type can be modeled as an enum with a single case or a singleton object with no state. In TypeScript, because of structural typing, add a private/unique property to prevent accidental structural compatibility when modeling these types.What is short-circuit evaluation and why does operand order matter?
Short-circuiting means the second operand of&& or || is evaluated only if needed. For a && b, if a is false, b isn’t evaluated; for a || b, if a is true, b isn’t evaluated. Order cheaper or safer checks first so expensive calls (like database queries) or side-effectful code can often be skipped.Why can 0.1 + 0.1 + 0.1 === 0.3 be false in JavaScript/TypeScript?
Numbers in JS/TS are IEEE‑754 binary64 floating‑point. Many decimal fractions (like 0.1) can’t be represented exactly in binary, so they’re stored as close approximations. Combining approximations can produce results like 0.30000000000000004, making strict equality with 0.3 false.How should I compare floating‑point numbers for equality?
Compare within a tolerance (epsilon), not with strict equality. For example, checkMath.abs(a - b) <= Number.EPSILON to treat two numbers as “equal within rounding error.” This accounts for small representation and rounding differences introduced by floating‑point arithmetic.What are integer overflow/underflow and how are they handled?
They occur when a result can’t be represented in the fixed number of bits. Common strategies are wrap-around (hardware default, fast but risky), saturation (clamp to min/max; safe but breaks algebraic laws), or erroring out (safest, but requires checks and error handling). When the default isn’t suitable, detect overflow/underflow with range checks before performing the operation.Why do emojis sometimes get “split” when slicing strings, and how can I avoid it?
What users see (graphemes) can be composed of multiple Unicode code points (characters), sometimes joined with special code points (e.g., zero‑width joiner). JavaScript’sstring.length counts UTF‑16 code units, not graphemes, so naive slicing can cut an emoji in half. Use libraries that operate at grapheme boundaries (e.g., grapheme‑splitter) to manipulate visible characters safely.
Programming with Types ebook for free