Skip to content
academia.sh

Lesson 02 / 23

Basic Types

The first typed version of the measurement record, using primitive types, array and readonly array notations, tuples, enums, and object types.

Contents

The previous lesson established what the type layer does and does not do. Next comes this layer’s vocabulary: to write the measurement record’s fields, the types available first have to be known.

TypeScript’s type vocabulary sits on top of JavaScript’s value model. It adds no new value kind; it names the kinds that already exist and gives ways to build compound types from them. This lesson introduces primitive types, arrays, tuples, and enums, and closes by writing the measurement record’s first typed version.

Primitive Types

Every primitive value introduced in the JavaScript Fundamentals course has a type name. Type names are written in lowercase:

const id: string = "s-01";
const value: number = 21.4;
const valid: boolean = true;
const description: string | null = null;
const bigCounter: bigint = 9007199254740993n;

console.log(typeof id, typeof value, typeof valid, typeof description, typeof bigCounter);
console.log(0.1 + 0.2 === 0.3);
console.log(Number.isInteger(value), Number.isSafeInteger(9007199254740993));

Output:

string number boolean object bigint
false
false false

Three observations are needed.

number is a single number type. There is no split between integer and real number; both are held in the IEEE 754 double-precision representation. The limits established in the Floating Point Numbers lesson of the How Computers Work course hold here exactly as they are — the false on the second line is a direct consequence. The type system does not remove this limit, it only guarantees that a number is a number.

typeof null is "object". This is an old inconsistency the language carries; the type system recognizes a separate type for null but does not change the runtime behavior. It is an example showing that types are erased: the null type the compiler sees and the value typeof sees come from the same place but do not say the same thing.

bigint and symbol are separate types. bigint is used where the safe-integer limit of the number type is exceeded, symbol for unique property keys. Neither can be confused with number; bigint and number cannot be added directly.

null and undefined are two separate types and, under the default strict configuration, cannot be assigned to other types on their own. This is why description is declared above as string | null; had it been declared as just string, the compiler would reject the assignment. This behavior depends on the strictNullChecks option and will be covered in detail in the Compiler Configuration lesson.

Arrays and Readonly Arrays

An array’s type is written by appending [] to the element type. An equivalent notation also exists:

const values: number[] = [21.4, 22.1, 23.0];
const ids: Array<string> = ["s-01", "s-02"];
const readonlyValues: readonly number[] = [21.4, 22.1];

console.log(values.length, ids.length, readonlyValues.length);
readonlyValues.push(23.0);

The compiler produces this diagnostic for the last line:

b2.ts(6,16): error TS2339: Property 'push' does not exist on type 'readonly number[]'.

number[] and Array<number> are the same type; the first is the short notation. readonly number[] is a different type: an array whose elements can be read but not changed. Mutating methods (push, pop, sort, splice) are not defined on this type, so calling them produces a “no such property” diagnostic.

The readonly here is the type-level counterpart of the immutable concept from the Programming Fundamentals course — but only at the type level. Nothing called readonly survives in compiled output; the array is an ordinary array at runtime, and code that has not passed type checking can mutate it. Real immutability requires runtime tools like Object.freeze.

Tuples

A tuple is an array whose element count and each position’s type are fixed. It suits representing the measurement record’s valid value range:

type Interval = [low: number, high: number];

const validInterval: Interval = [-40, 85];
const [low, high] = validInterval;
console.log(low, high);

const invalid: Interval = [-40, 85, 120];

The diagnostic for the last line:

b3.ts(7,7): error TS2322: Type '[number, number, number]' is not assignable to type 'Interval'.
  Source has 3 element(s) but target allows only 2.

The names low: and high: document the tuple’s elements; they have no runtime counterpart, and the array is still read by position. Their value is readability: [number, number] does not say which number comes first, [low: number, high: number] does.

The distinction between a tuple and an array is this: an array does not know how many elements there are, it knows they are all the same type; a tuple knows how many elements there are and knows the type of each one separately.

Fixed Value Sets: enum

The measurement record’s unit field can only take certain values. One way to name such a set is an enum:

enum Unit {
  Celsius = "C",
  Pascal = "Pa",
  Percent = "%",
}

const unit: Unit = Unit.Celsius;
console.log(unit, Unit.Pascal);

When this file is compiled with tsc --target es2022 --strict unit.ts, the JavaScript produced is:

"use strict";
var Unit;
(function (Unit) {
    Unit["Celsius"] = "C";
    Unit["Pascal"] = "Pa";
    Unit["Percent"] = "%";
})(Unit || (Unit = {}));
const unit = Unit.Celsius;
console.log(unit, Unit.Pascal);

Run, the output is C Pa.

This output shows an exception to the previous lesson’s rule: an enum is not erased. The compiler produces an object that exists at runtime. Unlike a type annotation, enum is not a type, it is a declaration producing both a type and a value.

A numeric enum makes the produced object do even more:

"use strict";
var Status;
(function (Status) {
    Status[Status["Valid"] = 0] = "Valid";
    Status[Status["Suspect"] = 1] = "Suspect";
    Status[Status["Invalid"] = 2] = "Invalid";
})(Status || (Status = {}));

This is a two-way mapping giving 1 for Status.Suspect and "Suspect" for Status[1]. It is a convenience, but it has a cost: every enum adds code to the bundle output, and cannot be produced correctly when a file is compiled on its own (without type information). For this reason the compiler has an option that allows only erasable syntax. With that option on:

status.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.

Which version the option is available from varies by compiler; its behavior is fixed — it rejects all TypeScript syntax that produces runtime code.

The Union Alternative

The same constraint can also be expressed in a way that leaves no runtime trace at all:

type Unit = "C" | "Pa" | "%";

const unit: Unit = "C";
console.log(unit);

const invalidUnit: Unit = "F";

The diagnostic for the last line:

b4.ts(6,7): error TS2322: Type '"F"' is not assignable to type 'Unit'.

The "C" here is not a string, it is a type: a single-element set containing only the value "C". Such types are called a literal type and will be covered in a separate lesson. The union of three literal types defines a three-valued set.

Comparison of the two approaches:

Criterion enum Union of literal types
Runtime code Produces it Does not produce it
Accessing a value Unit.Celsius "C"
Matching external data Requires conversion Direct
Enumerating all values Via the object Requires a separate array

This course will use the union of literal types, since it does not break the type erasure principle.

The Measurement Record’s First Typed Version

With the tools gathered, the model’s first typed version can be written. An object type lists field names and types inside curly braces:

type Measurement = {
  id: string;
  sensor: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
};

const measurement: Measurement = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};

console.log(measurement.id, measurement.value, measurement.unit);
console.log(measurement.location);

The diagnostic for the last line:

b5.ts(18,25): error TS2339: Property 'location' does not exist on type 'Measurement'.

The model now knows three things: which fields exist, each field’s type, and the unit field’s value set. What it does not know is also visible: the sensor field can hold any string, there is no range for the value field, and it is unclear whether the time field is a timestamp or a duration. These gaps will be closed in later topics.

Summary

  • Primitive types name JavaScript’s value model; number is a single number type and carries the limits of floating-point representation.
  • T[] and Array<T> are the same type; readonly T[] is a separate type that does not include mutating methods and only holds at compile time.
  • A tuple is an array whose element count and each position’s type are fixed; element names are for documentation.
  • An enum declaration produces runtime code and is the exception to the type erasure principle; a numeric enum also produces a two-way mapping.
  • A union of literal types expresses the same constraint without leaving a runtime trace.

Next Step

The measurement record is now typed, but data coming from the outside world has not entered this type yet. What is the type of the value JSON.parse returns, and how is that value safely converted to the model? The next lesson covers the type system’s two endpoints — any, which allows everything, and unknown, which allows nothing — and the never type, which has no values at all.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close