TypeScript Strict Basics
Types name what a value may be. Interfaces name object shapes. Narrowing proves which union member you hold.
<!-- hal:authoritative:yaml -->
Types name what a value may be. Interfaces name object shapes. Narrowing proves which member of a union you hold so the checker lets the next line through.
§I — Frame
Asr session 02 for the Bun/TypeScript track. Peer fill for 2026-09-10: Nix session 07 already shipped; this bundle authors the missing TypeScript peer only. Session 01 proved bun run on .ts without a separate transpile step, and drew the line between Bun's strip and tsc --noEmit. Today the line moves to the checker under strict.
Done-criteria from the syllabus: types, interfaces, and narrowing under strict.
Primary cites: Cherny Programming TypeScript Chapter 3 (All About Types) on disk at 09-Tomes/Polyglot-Dev/Web-Stack/TypeScript/, plus the TypeScript Handbook pages for Everyday Types and Narrowing. Bun docs stay secondary; no HTTP server, no package-manager depth, no generics deep dive.
Strict here means the usual strict: true bundle in tsconfig.json (which turns on strictNullChecks, noImplicitAny, and related flags). Session 01 already showed that Bun will still execute a file whose types are wrong. The proofs in this lesson are what tsc --noEmit (or your editor's language service) reports when those flags are on.
Home: Archmagus-Stack/Polyglot-Dev/TypeScript/. Do not dump into Polyglot-Dev/Web/.
§II — Types you write every day
Cherny opens Chapter 3 by treating a type as a set of values and the operations those values allow. The Handbook's Everyday Types page lists the same starter set you will annotate constantly.
Primitives map one-to-one with JavaScript's common typeof results:
let name: string = "asr";
let count: number = 2;
let ready: boolean = true;
Prefer the lowercase forms (string, number, boolean). The capitalized String / Number / Boolean names exist and almost never belong in annotations.
Arrays are written T[] or Array<T>:
const ids: number[] = [1, 2, 3];
Functions take parameter and return annotations when inference is not enough:
function greet(who: string): string {
return `hello, ${who}`;
}
Cherny's style note matches the Handbook: infer when the initializer already fixes the type; annotate when you need a bound the initializer cannot state. A naked let x; with no initializer becomes an implicit any under default settings. With noImplicitAny (part of strict), that becomes an error until you annotate or initialize.
Avoid any as a habit. Cherny calls any the type that opts out of checking. Under strict you want the opposite posture: every value either has an inferred type or an explicit one the checker can verify.
Type literals tighten a general type to one value. Cherny shows const c = true inferred as the literal true, while let a = true widens to boolean. Literal unions then name closed choice sets:
type Align = "left" | "right" | "center";
function place(a: Align): void {
console.log(a);
}
Unions are how you say "this value is one of these members." The Handbook rule is sharp: you may only use an operation that is valid for every member until you narrow.
function printId(id: number | string) {
// id.toUpperCase(); // error: not on number
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(0));
}
}
That typeof branch is the first narrowing move. Section IV expands the rest.
§III — Interfaces and structural shapes
Object types list properties and their types. The Handbook writes them inline or names them.
function printCoord(pt: { x: number; y: number }) {
console.log(pt.x, pt.y);
}
Optional properties use ?. Under strictNullChecks, reading an optional field means you must account for undefined before treating it as present:
function printName(obj: { first: string; last?: string }) {
console.log(obj.first);
if (obj.last !== undefined) {
console.log(obj.last.toUpperCase());
}
}
Cherny's object section adds the same discipline for definite assignment and optional fields: if you declare a required property, the checker expects it; if you mark it optional, callers may omit it and readers must prove it before use.
An interface names an object shape so you stop repeating the inline type:
interface Point {
x: number;
y: number;
}
function distance(a: Point, b: Point): number {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
TypeScript is structurally typed. Any value with compatible properties satisfies Point, whether or not it was constructed with that interface name. Cherny stresses the same idea when comparing type aliases and interfaces: both name shapes; the checker cares about the fields present, not about a nominal brand.
Handbook heuristic for this session: prefer interface for object shapes you will extend; use type when you need a union, a mapped form, or a rename of a non-object type (type Id = string | number). Interfaces can be reopened and merged; type aliases cannot. You do not need declaration merging today. You do need to stop pasting the same { x: number; y: number } into every signature.
interface Named {
name: string;
}
interface User extends Named {
id: number;
}
extends keeps the structural story: User is a shape with name and id. A plain object literal with those fields still assigns.
Type aliases remain useful beside interfaces. Name a union once and reuse it:
type Id = string | number;
type Result = { ok: true; value: string } | { ok: false; error: string };
Cherny treats aliases as names for any type, including unions and intersections. Interfaces stay for object shapes you expect to extend. Mixing both is normal: an interface field can reference a type alias, and a type alias can wrap an interface in a union. The mistake to avoid is inventing a new inline object type in every function when one named shape would do.
§IV — Narrowing under strict
Narrowing is control-flow refinement. The Handbook's Narrowing page is the operational map; Cherny's union discussion is the set-theory frame. You start with a wide type. After a guard the checker holds a tighter type in that branch.
**typeof guards.** Useful for primitives. Remember the JavaScript quirk the Handbook flags: typeof null === "object". A check for "object" does not remove null by itself when the union includes both arrays/objects and null.
Truthiness. if (value) narrows away null and undefined, and also away 0, "", and NaN. Prefer an explicit != null (or !== undefined / !== null) when zero or empty string are valid data.
Equality. === / !== against a literal or against another variable can collapse a union to the shared member. == null removes both null and undefined.
**in and instanceof.** "swim" in animal narrows by property presence. x instanceof Date narrows class instances.
Discriminated unions. Encode variants with a shared literal tag instead of a bag of optional fields:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
After shape.kind === "circle", radius is required. You do not need a non-null assertion. The Handbook contrasts this with a single interface where kind is a union and both radius and sideLength are optional: the tag does not prove which field exists, so strictNullChecks still complains.
**Exhaustiveness with never.** When every variant is handled, assign the remainder to never in a default so adding a new member fails the build:
function areaExhaustive(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default: {
const _dead: never = shape;
return _dead;
}
}
}
StrictNullChecks is why optional fields and null | T force these guards. Without it, null slips into almost every type and the narrowing story collapses. Keep strict: true. A function that returns string | null must be narrowed before you call .length. The same rule applies when a network payload might omit a field: model the absence in the type, then prove presence in the code.
Run the checker on purpose (bunx tsc --noEmit once typescript is a project dependency). Bun will still run the file; the lesson's proof is the diagnostic, not the process exit from bun run. Session 01 already taught that split. Session 02 asks you to treat a clean check as the ship gate for typed source.
§V — Self-check
**Proof 1. Everyday types under noImplicitAny.** Annotate or initialize so nothing falls to implicit any. If a parameter stayed untyped and the checker stayed quiet, strict is off or the value was inferred.
Proof 2. Interface as a structural name. Pass a fresh object literal with the required fields into a function typed on an interface. Extra required fields fail excess-property checks on fresh literals; missing fields fail assignability.
**Proof 3. Union plus typeof.** Call a function with string | number and use a method that exists on only one member. The checker must error before the guard and accept after it.
Proof 4. Discriminated union. Model two variants with a kind tag and required per-variant fields. Switch on kind and read a field that exists only on one variant without !.
Proof 5. Exhaustiveness. Add a third variant to the union and watch the never default fail until you handle it.
If any proof fails, re-read the matching section and the Handbook page it cites. Re-run tsc --noEmit. Do not treat a successful bun run as proof that the types are sound.
Closing
Session 02 seats the TypeScript language layer beside Bun's runtime. You named everyday types, preferred inference with explicit bounds where needed, and rejected casual any under noImplicitAny. You named object shapes with interface, trusted structural assignability, and extended shapes with extends. You narrowed unions with typeof, careful truthiness, equality, and discriminated tags, and you used never for exhaustiveness under strictNullChecks.
Cherny Chapter 3 supplies the vocabulary: types as sets, assignability, aliases, unions, object fields. The Handbook supplies the operational narrowing catalog you will use in every later Bun/TS fire. Session 01's split still holds: Bun strips to run; tsc checks to refuse bad programs before they ship.
Name the mechanism when someone asks what "strict TypeScript" buys on this track: the checker forces you to prove which union member you hold before you use member-specific operations, and interfaces give those proofs a reusable shape name.
Session 03 is Bun as package manager (install, lockfile, scripts). Do not start it in this folder. Do not write it today.
Examine well. Everyday types are the first proof. Interfaces are the second. Narrowing is the third. Discriminated unions are the fourth. Exhaustiveness is the fifth. The door is a tsc --noEmit that stays clean with strict: true.
Related
- Syllabus: Asr Bun/TS syllabus, session 02
- Syllabus: Asr Nix syllabus, peer track
- Grounding (live): https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
- Grounding (live): https://www.typescriptlang.org/docs/handbook/2/narrowing.html
- Primary tome: Cherny Programming TypeScript (2019)
- Prior fire: Asr session 01 · Bun runtime basics
- Nix peer (same day): Asr session 07 · automatic runtime deps
- Next fire: session 03, Bun as package manager (unwritten)
Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-10 · Asr · session 02 · TypeScript strict basics (types, interfaces, narrowing)