Warm-up · Activity 1 of 7
Warm-up from JavaScript: JSON.parse gives you whatever the text says. What does this print?
const data = JSON.parse('{ "price": "25" }');
console.log(data.price * 2, typeof data.price);// B5.5 · ~38 min · Beginner
After this lesson you can model nested data with every object type tool of this module, load it safely from JSON, and compute summaries without changing it.
Lesson 5 of 5 in B5 Object types
You will be able to
Warm-up · Activity 1 of 7
const data = JSON.parse('{ "price": "25" }');
console.log(data.price * 2, typeof data.price);Predict · Activity 2 of 7
interface Item {
name: string;
}
const raw = JSON.parse('{ "items": [{ "name": 42 }] }');
const items: Item[] = raw.items;
console.log(items[0].name.toUpperCase());Practice · Activity 3 of 7
interface Inventory {
items: string[];
}
const raw: ____ = JSON.parse('{ "items": ["lamp"] }');
// @ts-expect-error: check raw before you use it
console.log(raw.items);Practice · Activity 4 of 7
function tagsOf(value: unknown) {
if (Array.isArray(value)) {
// here
return value;
}
return [];
}Practice · Activity 5 of 7
interface Product {
readonly sku: string;
name: string;
note?: string;
}
interface Item extends Product {
tags: readonly string[];
location: readonly [string, number];
}
const lamp: Item = { sku: "L-1", name: "Lamp", tags: ["light"], location: ["A", 3] };
lamp.name = "Desk lamp";
lamp.note = "new";
lamp.tags = [...lamp.tags, "sale"];
lamp.location[1] = 4;
lamp.sku = "L-2";Brain teaser · Activity 6 of 7
function isRecord(value: unknown): value is { [key: string]: unknown } {
return typeof value === "object";
}
function skuOf(value: unknown): string {
if (!isRecord(value)) throw new Error("not an object");
const { sku } = value;
return typeof sku === "string" ? sku : "no sku";
}
console.log(skuOf(JSON.parse("null")));Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
The whole module in one program. DATA is the JSON text. The model uses every tool of B5. The predicates and parseItem check an unknown value field by field; parseInventory checks the outer object and its items array. totalStock and the value read the data and change nothing. The last call loads an item whose price is "cheap". Run npx tsc, then node main.ts. Then change "B" in DATA to 2 and run it again.
main.ts
// The data, as it might arrive from a file or a server.
const DATA = `{
"updated": "2026-10-04",
"items": [
{ "sku": "L-1", "name": "Desk lamp", "price": 25, "location": ["A", 3],
"tags": ["light"], "stock": { "berlin": 4, "bonn": 2 } },
{ "sku": "C-7", "name": "Chair", "price": 80, "note": "assembly needed",
"location": ["B", 1], "tags": ["office", "seating"], "stock": { "berlin": 1 } }
]
}`;
// The model: every idea from module B5.
interface Product {
readonly sku: string;
name: string;
price: number;
note?: string;
}
interface Stock {
readonly [warehouse: string]: number;
}
type Location = readonly [string, number]; // aisle, shelf
interface Item extends Product {
location: Location;
tags: readonly string[];
stock: Stock;
}
interface Inventory {
readonly updated: string;
readonly items: readonly Item[];
}
// Checks: from unknown to the model, one property at a time.
function fail(message: string): never {
throw new Error(message);
}
function isRecord(value: unknown): value is { [key: string]: unknown } {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isLocation(value: unknown): value is Location {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((v) => typeof v === "string");
}
function isStock(value: unknown): value is Stock {
return isRecord(value) && Object.values(value).every((n) => typeof n === "number");
}
function parseItem(value: unknown): Item {
if (!isRecord(value)) fail("an item must be an object");
const { sku, name, price, note, location, tags, stock } = value;
if (typeof sku !== "string") fail("sku must be a string");
if (typeof name !== "string") fail("name must be a string");
if (typeof price !== "number") fail("price must be a number");
if (!isLocation(location)) fail("location must be [aisle, shelf]");
if (!isStringArray(tags)) fail("tags must be strings");
if (!isStock(stock)) fail("stock must map warehouses to numbers");
const item: Item = { sku, name, price, location, tags, stock };
if (note !== undefined) {
if (typeof note !== "string") fail("note must be a string");
item.note = note;
}
return item;
}
function parseInventory(text: string): Inventory {
const raw: unknown = JSON.parse(text);
if (!isRecord(raw)) fail("the inventory must be an object");
const { updated, items } = raw;
if (typeof updated !== "string") fail("updated must be a string");
if (!Array.isArray(items)) fail("items must be an array");
return { updated, items: items.map(parseItem) };
}
// Summaries: read the data, never change it.
function totalStock(item: Item): number {
let total = 0;
for (const count of Object.values(item.stock)) total += count;
return total;
}
const inventory = parseInventory(DATA);
for (const item of inventory.items) {
const [aisle, shelf] = item.location;
console.log(item.sku + " " + item.name + ": " + totalStock(item) + " in stock, aisle " + aisle + " shelf " + shelf + (item.note ? " (" + item.note + ")" : ""));
}
let value = 0;
for (const item of inventory.items) value += item.price * totalStock(item);
console.log("value: " + value + ", updated " + inventory.updated);
try {
parseInventory('{ "updated": "2026-10-04", "items": [{ "sku": "X-1", "name": "Shelf", "price": "cheap" }] }');
} catch (error) {
console.log("rejected: " + (error instanceof Error ? error.message : String(error)));
}
Run it with
npx tsc
node main.tsOutput
L-1 Desk lamp: 6 in stock, aisle A shelf 3
C-7 Chair: 1 in stock, aisle B shelf 1 (assembly needed)
value: 230, updated 2026-10-04
rejected: price must be a numberTab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
Exercise 1 of 3
Fix the model, so that lamp fits it and every // @ts-expect-error in noEdits finds its error. note becomes optional. stock becomes an exported interface Stock with a readonly index signature: warehouse names to counts. tags becomes a readonly string[], and location a readonly tuple of aisle and shelf. Then make totalStock add the counts of all warehouses, so that the lamp's total is 6. Run the tests: the first one is the type-check.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
export interface Stock { readonly [warehouse: string]: number; } and then stock: Stock in Item.
readonly goes in front of both array types: tags: readonly string[]; location: readonly [string, number];
let total = 0; for (const count of Object.values(item.stock)) total += count; return total;
One way to solve it. Yours can look different and still pass the checks.
export interface Product {
readonly sku: string;
name: string;
price: number;
note?: string;
}
export interface Stock {
readonly [warehouse: string]: number;
}
export interface Item extends Product {
location: readonly [string, number];
tags: readonly string[];
stock: Stock;
}
export function totalStock(item: Item): number {
let total = 0;
for (const count of Object.values(item.stock)) total += count;
return total;
}
export function stockValue(items: readonly Item[]): number {
let value = 0;
for (const item of items) value += item.price * totalStock(item);
return value;
}
export const lamp: Item = {
sku: "L-1",
name: "Desk lamp",
price: 25,
location: ["A", 3],
tags: ["light"],
stock: { berlin: 4, bonn: 2 }
};
function noEdits(item: Item) {
// @ts-expect-error: tags are read-only
item.tags.push("sale");
// @ts-expect-error: the stock is read-only
item.stock.berlin = 0;
// @ts-expect-error: a location cannot change
item.location[1] = 4;
}
Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.ts
export interface Product {
readonly sku: string;
name: string;
price: number;
note: string;
}
export interface Item extends Product {
location: [string, number];
tags: string[];
stock: number;
}
export function totalStock(item: Item): number {
return item.stock;
}
export function stockValue(items: readonly Item[]): number {
let value = 0;
for (const item of items) value += item.price * totalStock(item);
return value;
}
export const lamp: Item = {
sku: "L-1",
name: "Desk lamp",
price: 25,
location: ["A", 3],
tags: ["light"],
stock: { berlin: 4, bonn: 2 }
};
function noEdits(item: Item) {
// @ts-expect-error: tags are read-only
item.tags.push("sale");
// @ts-expect-error: the stock is read-only
item.stock.berlin = 0;
// @ts-expect-error: a location cannot change
item.location[1] = 4;
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {totalStock, stockValue, lamp} from './main.ts';
const chair = {sku: 'C-7', name: 'Chair', price: 80, location: ['B', 1] as const, tags: [], stock: {berlin: 1}};
const empty = {sku: 'E-0', name: 'Empty box', price: 2, location: ['C', 2] as const, tags: [], stock: {}};
test('totalStock adds the counts of every warehouse', () => {
assert.equal(totalStock(lamp), 6, `totalStock(lamp) gave ${totalStock(lamp)}`);
});
test('an item with no stock entries has 0 in stock', () => {
assert.equal(totalStock(empty), 0, `totalStock(empty) gave ${totalStock(empty)}`);
});
test('stockValue adds price times stock over all items', () => {
const got = stockValue([lamp, chair, empty]);
assert.equal(got, 230, `stockValue([lamp, chair, empty]) gave ${got}`);
});
package.json
{
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"lib": [
"esnext",
"dom"
],
"types": [],
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.
Run the program:
npx tsc
node main.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 2 of 3
parseItem checks sku, name and price, but isLocation and isStock check too little and return a plain boolean, so tsc cannot narrow location and stock, and tags is not checked at all. Turn both into type predicates. isLocation: exactly two elements, a string and a number. isStock: a record whose values are all numbers. Add a predicate for tags, an array of strings, and fail with tags must be strings when it does not hold.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
A predicate is a return type: function isLocation(value: unknown): value is Location {
Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number"
isRecord(value) && Object.values(value).every((count) => typeof count === "number") checks every warehouse.
One way to solve it. Yours can look different and still pass the checks.
export interface Product {
readonly sku: string;
name: string;
price: number;
note?: string;
}
export interface Stock {
readonly [warehouse: string]: number;
}
export type Location = readonly [string, number];
export interface Item extends Product {
location: Location;
tags: readonly string[];
stock: Stock;
}
function fail(message: string): never {
throw new Error(message);
}
function isRecord(value: unknown): value is { [key: string]: unknown } {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function isLocation(value: unknown): value is Location {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}
export function isStock(value: unknown): value is Stock {
return isRecord(value) && Object.values(value).every((count) => typeof count === "number");
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((tag) => typeof tag === "string");
}
export function parseItem(value: unknown): Item {
if (!isRecord(value)) fail("an item must be an object");
const { sku, name, price, location, tags, stock } = value;
if (typeof sku !== "string") fail("sku must be a string");
if (typeof name !== "string") fail("name must be a string");
if (typeof price !== "number") fail("price must be a number");
if (!isLocation(location)) fail("location must be [aisle, shelf]");
if (!isStringArray(tags)) fail("tags must be strings");
if (!isStock(stock)) fail("stock must map warehouses to numbers");
return { sku, name, price, location, tags, stock };
}
Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.ts
export interface Product {
readonly sku: string;
name: string;
price: number;
note?: string;
}
export interface Stock {
readonly [warehouse: string]: number;
}
export type Location = readonly [string, number];
export interface Item extends Product {
location: Location;
tags: readonly string[];
stock: Stock;
}
function fail(message: string): never {
throw new Error(message);
}
function isRecord(value: unknown): value is { [key: string]: unknown } {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function isLocation(value: unknown): boolean {
return Array.isArray(value);
}
export function isStock(value: unknown): boolean {
return isRecord(value);
}
export function parseItem(value: unknown): Item {
if (!isRecord(value)) fail("an item must be an object");
const { sku, name, price, location, tags, stock } = value;
if (typeof sku !== "string") fail("sku must be a string");
if (typeof name !== "string") fail("name must be a string");
if (typeof price !== "number") fail("price must be a number");
if (!isLocation(location)) fail("location must be [aisle, shelf]");
if (!isStock(stock)) fail("stock must map warehouses to numbers");
return { sku, name, price, location, tags, stock };
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {isLocation, isStock, parseItem} from './main.ts';
const lamp = {sku: 'L-1', name: 'Desk lamp', price: 25, location: ['A', 3], tags: ['light'], stock: {berlin: 4, bonn: 2}};
test('isLocation accepts an aisle and a shelf, and nothing else', () => {
assert.equal(isLocation(['A', 3]), true, "isLocation(['A', 3]) should be true");
assert.equal(isLocation(['A', '3']), false, "isLocation(['A', '3']) should be false: the shelf is a number");
assert.equal(isLocation(['A', 3, 1]), false, "isLocation(['A', 3, 1]) should be false: exactly two parts");
});
test('isStock accepts only numbers per warehouse', () => {
assert.equal(isStock({berlin: 4}), true, 'isStock({berlin: 4}) should be true');
assert.equal(isStock({berlin: 'four'}), false, "isStock({berlin: 'four'}) should be false");
assert.equal(isStock([4]), false, 'isStock([4]) should be false: an array is not a map of warehouses');
});
test('a valid item comes back with all its properties', () => {
assert.deepEqual(parseItem(lamp), lamp, 'parseItem(lamp) should return an object equal to lamp');
});
test('tags must be an array of strings', () => {
assert.throws(() => parseItem({...lamp, tags: ['light', 7]}), {message: 'tags must be strings'}, 'a tag 7 should throw "tags must be strings"');
});
test('a bad location or stock is rejected with its message', () => {
assert.throws(() => parseItem({...lamp, location: ['A']}), {message: 'location must be [aisle, shelf]'}, "location ['A'] should throw \"location must be [aisle, shelf]\"");
assert.throws(() => parseItem({...lamp, stock: {berlin: '4'}}), {message: 'stock must map warehouses to numbers'}, "stock {berlin: '4'} should throw \"stock must map warehouses to numbers\"");
});
package.json
{
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"lib": [
"esnext",
"dom"
],
"types": [],
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.
Run the program:
npx tsc
node main.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 3 of 3
parseInventory trusts JSON.parse, and lowStock sorts the readonly items in place, which tsc rejects with error TS2339. Make parseInventory read the text as unknown and check it: fail with the inventory must be an object, updated must be a string or items must be an array, and check every item with parseItem. lowStock returns the SKUs whose total stock is at or below the limit, sorted, without changing the order of the items.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
const raw: unknown = JSON.parse(text); then if (!isRecord(raw)) fail("the inventory must be an object");
After if (!Array.isArray(items)) fail(…), items.map(parseItem) checks each item and gives an Item[].
toSorted returns a sorted copy and leaves inventory.items alone.
One way to solve it. Yours can look different and still pass the checks.
export interface Product {
readonly sku: string;
name: string;
price: number;
note?: string;
}
export interface Stock {
readonly [warehouse: string]: number;
}
export type Location = readonly [string, number];
export interface Item extends Product {
location: Location;
tags: readonly string[];
stock: Stock;
}
export interface Inventory {
readonly updated: string;
readonly items: readonly Item[];
}
function fail(message: string): never {
throw new Error(message);
}
function isRecord(value: unknown): value is { [key: string]: unknown } {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isLocation(value: unknown): value is Location {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}
function isStock(value: unknown): value is Stock {
return isRecord(value) && Object.values(value).every((count) => typeof count === "number");
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((tag) => typeof tag === "string");
}
export function parseItem(value: unknown): Item {
if (!isRecord(value)) fail("an item must be an object");
const { sku, name, price, location, tags, stock } = value;
if (typeof sku !== "string") fail("sku must be a string");
if (typeof name !== "string") fail("name must be a string");
if (typeof price !== "number") fail("price must be a number");
if (!isLocation(location)) fail("location must be [aisle, shelf]");
if (!isStringArray(tags)) fail("tags must be strings");
if (!isStock(stock)) fail("stock must map warehouses to numbers");
return { sku, name, price, location, tags, stock };
}
export function totalStock(item: Item): number {
let total = 0;
for (const count of Object.values(item.stock)) total += count;
return total;
}
export function parseInventory(text: string): Inventory {
const raw: unknown = JSON.parse(text);
if (!isRecord(raw)) fail("the inventory must be an object");
const { updated, items } = raw;
if (typeof updated !== "string") fail("updated must be a string");
if (!Array.isArray(items)) fail("items must be an array");
return { updated, items: items.map(parseItem) };
}
export function lowStock(inventory: Inventory, limit: number): readonly string[] {
const sorted = inventory.items.toSorted((a, b) => a.sku.localeCompare(b.sku));
return sorted.filter((item) => totalStock(item) <= limit).map((item) => item.sku);
}
Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.ts
export interface Product {
readonly sku: string;
name: string;
price: number;
note?: string;
}
export interface Stock {
readonly [warehouse: string]: number;
}
export type Location = readonly [string, number];
export interface Item extends Product {
location: Location;
tags: readonly string[];
stock: Stock;
}
export interface Inventory {
readonly updated: string;
readonly items: readonly Item[];
}
function fail(message: string): never {
throw new Error(message);
}
function isRecord(value: unknown): value is { [key: string]: unknown } {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isLocation(value: unknown): value is Location {
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number";
}
function isStock(value: unknown): value is Stock {
return isRecord(value) && Object.values(value).every((count) => typeof count === "number");
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((tag) => typeof tag === "string");
}
export function parseItem(value: unknown): Item {
if (!isRecord(value)) fail("an item must be an object");
const { sku, name, price, location, tags, stock } = value;
if (typeof sku !== "string") fail("sku must be a string");
if (typeof name !== "string") fail("name must be a string");
if (typeof price !== "number") fail("price must be a number");
if (!isLocation(location)) fail("location must be [aisle, shelf]");
if (!isStringArray(tags)) fail("tags must be strings");
if (!isStock(stock)) fail("stock must map warehouses to numbers");
return { sku, name, price, location, tags, stock };
}
export function totalStock(item: Item): number {
let total = 0;
for (const count of Object.values(item.stock)) total += count;
return total;
}
export function parseInventory(text: string): Inventory {
const raw = JSON.parse(text);
return { updated: raw.updated, items: raw.items };
}
export function lowStock(inventory: Inventory, limit: number): readonly string[] {
const sorted = inventory.items.sort((a, b) => a.sku.localeCompare(b.sku));
return sorted.filter((item) => totalStock(item) <= limit).map((item) => item.sku);
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseInventory, lowStock} from './main.ts';
const item = (sku, berlin) => ({sku, name: sku, price: 1, location: ['A', 1], tags: [], stock: {berlin}});
const text = JSON.stringify({updated: '2026-10-04', items: [item('L-1', 6), item('C-7', 1), item('B-2', 3)]});
test('a valid inventory is loaded with all its items', () => {
const inventory = parseInventory(text);
assert.equal(inventory.updated, '2026-10-04', `updated is ${JSON.stringify(inventory.updated)}`);
assert.deepEqual(inventory.items.map((i) => i.sku), ['L-1', 'C-7', 'B-2'], 'the three items should be loaded in order');
});
test('the inventory itself is checked', () => {
assert.throws(() => parseInventory('null'), {message: 'the inventory must be an object'}, 'null should throw "the inventory must be an object"');
assert.throws(() => parseInventory('{"updated": "2026-10-04", "items": 3}'), {message: 'items must be an array'}, 'items: 3 should throw "items must be an array"');
});
test('every item is checked too', () => {
const bad = JSON.stringify({updated: '2026-10-04', items: [{...item('X-1', 1), price: 'cheap'}]});
assert.throws(() => parseInventory(bad), {message: 'price must be a number'}, 'an item with price "cheap" should throw "price must be a number"');
});
test('lowStock lists the SKUs at or below the limit, sorted', () => {
const got = lowStock(parseInventory(text), 3);
assert.deepEqual(got, ['B-2', 'C-7'], `lowStock(inventory, 3) gave ${JSON.stringify(got)}`);
});
test('lowStock leaves the order of the items alone', () => {
const inventory = parseInventory(text);
lowStock(inventory, 3);
assert.deepEqual(inventory.items.map((i) => i.sku), ['L-1', 'C-7', 'B-2'], 'after lowStock the items should still be in their order');
});
package.json
{
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"lib": [
"esnext",
"dom"
],
"types": [],
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.
Run the program:
npx tsc
node main.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsinterface Inventory {
items: { sku: string }[];
}
const inventory: Inventory = JSON.parse('{ "products": [] }');
console.log(inventory.items.length);
What tsc or Node.js prints
TypeError: Cannot read properties of undefined (reading 'length')Why, and the fix
JSON.parse returns any, so tsc accepts the annotation without looking at the text, and the mistake shows up only when the program runs. Here the text has products, not items. Read the result as unknown and check it before you build an Inventory from it, as parseInventory does; then a wrong file stops with a clear message such as items must be an array.
function skuOf(value: unknown): string {
return value.sku;
}
What tsc or Node.js prints
main.ts(2,10): error TS18046: 'value' is of type 'unknown'.Why, and the fix
unknown allows nothing until you check what it is; that is its purpose. Check that it is an object first, with a predicate such as isRecord, then check the property itself: if (!isRecord(value)) fail("not an object"); const { sku } = value; if (typeof sku !== "string") fail("sku must be a string"); return sku;
interface Item {
sku: string;
}
interface Inventory {
readonly items: readonly Item[];
}
function addItem(inventory: Inventory, item: Item): void {
inventory.items.push(item);
}
What tsc or Node.js prints
main.ts(9,19): error TS2339: Property 'push' does not exist on type 'readonly Item[]'.Why, and the fix
The loaded inventory is readonly on purpose: summaries must not change it. To add an item, build a new inventory instead: function addItem(inventory: Inventory, item: Item): Inventory { return { ...inventory, items: [...inventory.items, item] }; }. The old one stays as it was, which also keeps earlier summaries valid.
TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.