Skip to content
The Switch · JavaScript to TypeScript · Lesson 9

Data from outside

Check the parcel at the door before you sign for it.

Contents
Part 1 of 7 · The hook

Sign at the door

A parcel's label is a claim. Open it before you trust it.

A courier rings your bell. The parcel's label says "Books, handle with care". You sign, take it in, and open it later. Inside there is a pressure cooker.

The label was only a claim. The sender wrote it, and nobody checked. If you needed books, you found out too late.

Your TypeScript types are labels like that. Inside your own code, the checker looks at every parcel. But data that arrives from outside is different. A reply from a server. A file you read. Text a user typed. Whatever JSON.parse gives you. Your program didn't build these, so it can't know what is inside.

Here is the catch from lesson 0, now in full. Types exist only while you write the code. When the program runs, they are gone. The checker is not there at the door. So if you write "this is an Order" about data from a server, you have only written a label. You didn't check the parcel.

The honest approach is to treat outside data as unknown, and open it at the door. You check its shape at runtime, with ordinary code, and only then call it an Order. This lesson shows how, in a small amount of code, with no library.

Remember: types are labels, and labels can lie. Data from outside has to be checked by code that runs.

Part 2 of 7 · In JavaScript

Believing the label

JavaScript takes the parcel as it comes.

Here is a reply from a server, as text. It is meant to be an order with a number id, but this one carries the id as text.

JavaScriptOpen in TypeScript Playground (opens in a new tab)
const text = '{"id": "7", "item": "chai"}';const order = JSON.parse(text);console.log(order.id + 1);
Output
71

We wanted 7 + 1, which is 8. But the id arrived as the text "7", so + joined the two and gave "71". JavaScript had no way to tell us.

The same thing happens with fetch, the built-in function that asks a server for data. Its answer has a .json() method that turns the reply into an object.

JavaScriptOpen in TypeScript Playground (opens in a new tab)
fetch(url)  .then((response) => response.json())  .then((order) => console.log(order.item.toUpperCase()));

If the server sends an order with no item, the last line crashes on undefined. Whatever comes back is used as if it were right.

Remember: in JavaScript every reply is used as is, so a wrong reply fails later, somewhere else, and not where it came in.

Part 3 of 7 · In TypeScript

Unknown, then checked

Let the type say 'I don't know yet'.

Now the same code with a type on top.

const text = '{"id": "7", "item": "chai"}';const order = JSON.parse(text);console.log(order.id + 1);

Tinted lines are the ones that differ

Look closely. TypeScript said nothing, and the output is still 71. The type says order.id is a number, and the data says otherwise. This is the trap. JSON.parse returns the type any, and any fits into every label, so the checker lets it through. The label was a lie, and the checker had no way to know.

any is a hole in the checker. Anything you do with an any value is allowed, including things that crash.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
const loose: any = "chai";loose.nothing.here();

That compiles. It crashes the moment it runs, because "chai" has no property called nothing.

The safer partner is unknown. It means "a value of a kind I haven't found out yet". You can hold it, pass it around, and test it. But you can't use it until you check what it is.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
function shout(value: unknown): string {  return value.toUpperCase();}

TypeScript says

error TS18046: 'value' is of type 'unknown'.

You also can't hand it to a place that wants a specific type.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
const data: unknown = JSON.parse('{"item": "chai"}');const item: string = data;

TypeScript says

error TS2322: Type 'unknown' is not assignable to type 'string'.

Both errors are the checker saying "open the parcel first". And you open it with the narrowing from lesson 5.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
function describe(value: unknown): string {  if (typeof value === "string") {    return value.toUpperCase();  }  if (typeof value === "number") {    return value.toFixed(1);  }  return "something else";}console.log(describe("chai"));console.log(describe(21));console.log(describe(null));
Output
CHAI
21.0
something else

Each typeof check narrows value from unknown to something real. So unknown is any with the safety on. That is why a good rule is: data from outside is unknown until you check it.

Remember: any switches the checker off and unknown makes it wait for a check. Use unknown for anything you didn't make yourself.

Part 4 of 7 · How it works

Write a type guard

A small function that opens the parcel for you.

Checking each field by hand every time is tiring. So you write the check once, as a function that answers "is this an Order?". It has a special kind of return type called a type predicate, written value is Order. It tells TypeScript: "if this function says true, treat the value as an Order from then on". A function like this is called a type guard.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Order = { id: number; item: string };function isOrder(value: unknown): value is Order {  return (    typeof value === "object" &&    value !== null &&    "id" in value &&    typeof value.id === "number" &&    "item" in value &&    typeof value.item === "string"  );}function parseOrder(text: string): Order | undefined {  const data: unknown = JSON.parse(text);  return isOrder(data) ? data : undefined;}console.log(parseOrder('{"id": 7, "item": "chai"}'));console.log(parseOrder('{"id": "7", "item": "chai"}'));
Output
{ id: 7, item: 'chai' }
undefined

Read the guard from the top. First, is it an object, and not null? (Remember typeof null is "object".) Then, does it have an id property, and is that a number? Then the same for item. Only when every line passes does it return true.

The "id" in value check does two jobs. It tests that the property is there, and it tells TypeScript the property can be read, so typeof value.id is allowed on a value that started as unknown.

The second call in the example, where id was text, is turned away. The bad data stops at the door, and returns undefined, which the rest of the program must handle (lesson 6).

Lists need one more step. Check it is a list, then check every item with the guard you already have.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Order = { id: number; item: string };function isOrder(value: unknown): value is Order {  return (    typeof value === "object" &&    value !== null &&    "id" in value &&    typeof value.id === "number" &&    "item" in value &&    typeof value.item === "string"  );}function isOrderList(value: unknown): value is Order[] {  return Array.isArray(value) && value.every(isOrder);}console.log(isOrderList([{ id: 1, item: "chai" }]));console.log(isOrderList([{ id: 1, item: "chai" }, { id: "2" }]));console.log(isOrderList("chai"));
Output
true
false
false

Fetch is the same pattern. response.json() hands back an any, so the first thing you do is put it in an unknown variable. Then the guard does its work. Notice the response.ok check too. fetch only fails by itself when the network is down. A reply of "404 not found" arrives as a normal reply, and ok tells you whether the status was a success.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Order = { id: number; item: string };function isOrder(value: unknown): value is Order {  return (    typeof value === "object" &&    value !== null &&    "id" in value &&    typeof value.id === "number" &&    "item" in value &&    typeof value.item === "string"  );}async function loadOrder(address: string): Promise<Order | undefined> {  const response = await fetch(address);  if (!response.ok) {    return undefined;  }  const data: unknown = await response.json();  return isOrder(data) ? data : undefined;}const done = loadOrder("https://example.com/orders/7").then((order) => console.log(order));

Caught errors are outside data as well. In strict mode, the error in catch (error) is unknown, because anything can be thrown. Check what it is before you use it.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Order = { id: number; item: string };function parseOrderSafely(text: string): Order | string {  try {    return JSON.parse(text) as Order;  } catch (error) {    if (error instanceof SyntaxError) {      return "That was not valid JSON";    }    throw error;  }}console.log(parseOrderSafely('{"id": 7, "item": "chai"}'));console.log(parseOrderSafely("{not json"));
Output
{ id: 7, item: 'chai' }
That was not valid JSON

SyntaxError is the error JSON.parse throws for text that isn't JSON. We check for that kind with instanceof, and rethrow anything else, because we don't know what it is.

Remember: a type guard is a function that returns value is Type. It checks at runtime, and TypeScript trusts its answer afterwards.

Part 5 of 7 · Watch out

Where guards slip

Lying guards, quiet casts, and the check you skipped.

A guard is only as honest as its body. TypeScript trusts the promise in value is Order and never reads the code inside. This guard compiles, and it is useless.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Order = { id: number; item: string };// This compiles. The checker believes the promise in the signature and never inspects the body.function isOrder(value: unknown): value is Order {  return true;}const data: unknown = { nothing: "useful" };if (isOrder(data)) {  console.log(data.item.toUpperCase());}

It says yes to anything, so the next line crashes. When you write a guard, test it with good data and with bad data. A wrong guard is worse than none, because everybody downstream believes it.

as is not a check. Writing data as Order doesn't test anything. It tells the checker "trust me". It is the same as the label on the parcel. Lesson 11 covers when it is fair.

Check every field the code reads. If you use order.item, the guard has to check that item is text. A guard that checks only id lets a missing item slip through.

Extra fields are fine, missing and wrong ones are not. Your guard doesn't need to reject extra properties. The server may add new fields next year, and your code will keep working.

Big or growing shapes are tiring to guard by hand. At some size, teams reach for a validation library. You don't need one to start, and now you know what it is doing for you.

Remember: treat outside data as unknown, write a guard that checks everything you read, and test the guard.

Part 6 of 7 · Practise

Three parcels, opened

A name, a train ticket, and a list of scores.

Each problem starts with a JavaScript function that trusts what it gets. Add the check.

Problem 1

Read a name, safely

Type readName so it takes unknown, gives the name in capitals when there is a text name, and gives undefined for anything else.

Start here

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function readName(data) {  return data.name.toUpperCase();}console.log(readName({ name: "asha" })); // "ASHA"// readName(null) would crash, and so would readName({ name: 42 })

Stuck?

Show a solution
TypeScriptOpen in TypeScript Playground (opens in a new tab)
function readName(data: unknown): string | undefined {  if (typeof data === "object" && data !== null && "name" in data && typeof data.name === "string") {    return data.name.toUpperCase();  }  return undefined;}readName({ name: "asha" }); // "ASHA"readName(null); // undefinedreadName({ name: 42 }); // undefined

Starting from unknown forces the checks. Each part narrows the value a little more: object, then not null, then has name, then name is text. Only then is data.name known to be a string and toUpperCase allowed. The JavaScript version crashed on null and on a name that was a number.

Problem 2

Check a train ticket

Write the Ticket type and an isTicket guard, and make parseTicket return a Ticket or undefined. The ticket whose seat is text should give undefined.

Start here

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function parseTicket(json) {  return JSON.parse(json);}console.log(parseTicket('{"coach": "B2", "seat": 14}')); // { coach: 'B2', seat: 14 }console.log(parseTicket('{"coach": "B2", "seat": "14"}')); // seat is text, and nothing says so

Stuck?

Show a solution
TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Ticket = { coach: string; seat: number };function isTicket(value: unknown): value is Ticket {  return (    typeof value === "object" &&    value !== null &&    "coach" in value &&    typeof value.coach === "string" &&    "seat" in value &&    typeof value.seat === "number"  );}function parseTicket(json: string): Ticket | undefined {  const data: unknown = JSON.parse(json);  return isTicket(data) ? data : undefined;}parseTicket('{"coach": "B2", "seat": 14}'); // { coach: "B2", seat: 14 }parseTicket('{"coach": "B2", "seat": "14"}'); // undefined

The guard checks both fields, so a seat sent as the text '14' is turned away at the door. parseTicket keeps the parsed data as unknown until the guard says it is a Ticket. The JavaScript version handed back whatever came in, seat as text and all.

Problem 3

Load a list of scores

Type loadScores so it always gives back a list of numbers. A bad status or a bad reply should give an empty list.

Start here

JavaScriptOpen in TypeScript Playground (opens in a new tab)
async function loadScores(url) {  const response = await fetch(url);  return response.json();}

Stuck?

Show a solution
TypeScriptOpen in TypeScript Playground (opens in a new tab)
function isNumberList(value: unknown): value is number[] {  return Array.isArray(value) && value.every((item) => typeof item === "number");}async function loadScores(url: string): Promise<number[]> {  const response = await fetch(url);  if (!response.ok) {    return [];  }  const data: unknown = await response.json();  return isNumberList(data) ? data : [];}

The reply is unknown until the guard has looked at every item. A failed status, an object instead of a list, or a list with a text item all give an empty list, so the rest of the program can rely on getting numbers. The JavaScript version returned whatever json() gave.

Part 7 of 7 · Interview notes

Outside data in the interview

unknown, any, guards, and the erased types.

This is the question that separates people who have used TypeScript on real projects from people who have only read about it.

  • "What is the difference between any and unknown?" Both accept any value. any also lets you do anything with it, with no checks. unknown lets you do nothing until you narrow it.
  • "Does TypeScript check data at runtime?" No. Types are erased when the code is compiled. Anything from a server, a file or user input needs a runtime check.
  • "What does JSON.parse return?" any. So assign it to unknown and check it.
  • "What is a type guard?" A function whose return type is a type predicate, such as value is Order. When it returns true, TypeScript narrows the value to that type.
  • "How do you type a fetch call?" The reply's .json() is any. Put it in unknown, validate it with a guard, and handle the case where it fails.
  • "What does fetch do on a 404?" It still resolves. You must check response.ok.
  • "What type is the error in catch?" unknown, in strict mode. Narrow it, for example with instanceof Error, before using it.
  • "Is as Order a good way to type JSON?" No, because it checks nothing. It is a promise you can't keep if the server changes.
  • Say it simply. "Types disappear at runtime, so I treat outside data as unknown and check it with a type guard" is a good answer.

Updated 2026-10-17

Made by VeraStack Labs