Skip to content
Theory · Async code and outside data · lesson 7 of 10

Data from outside

At the customs border from the first theme, tsc checks the declarations of parcels that cross the border: the types in your code. But some parcels enter the country bypassing the border: they are brought in while the program is already running. For example:

  • a server's reply;
  • a string that JSON.parse reads;
  • the contents of a file.

Such parcels have no declaration. tsc never saw them: when it checked the program, this data did not exist yet.

If you write a declaration for such a parcel yourself, tsc takes your word for it:

TypeScript
type User = { name: string; age: number };

const user: User = JSON.parse('{"title": "report"}');
console.log(user.name.toUpperCase()); // ✗
TEXT
TypeError: Cannot read properties of undefined (reading 'toUpperCase')

tsc is silent. JSON.parse returns any, and any fits any type, so the annotation User passes without question. But the string has no field name, and at run time user.name turns out to be undefined. The annotation checked nothing: the types are erased, and not a trace of User is left in the JavaScript.

Data from outside goes into unknown

The safe order is different: everything that came from outside goes into a variable of type unknown. Then tsc does not let you use the data without a check:

TypeScript
const data: unknown = JSON.parse('{"title": "report"}');
console.log(data.title); // ✗
TEXT
error TS18046: 'data' is of type 'unknown'.

The parcel with no declaration stays sealed until someone opens it. In return the error shows up before the program runs, not in front of a user.

A check with a type predicate

Checking an object's shape right inside every if is clumsy. The check goes into a function with the type predicate v is User: at run time it looks at the value, and tsc narrows the type by its answer:

TypeScript
type User = { name: string; age: number };

function isUser(v: unknown): v is User {
    return typeof v === "object" && v !== null
        && "name" in v && typeof v.name === "string"
        && "age" in v && typeof v.age === "number";
}

const data: unknown = JSON.parse('{"name": "Anna", "age": 30}');
if (isUser(data)) {
    console.log(data.name.toUpperCase()); // ANNA
} else {
    console.log("not a user");
}

Let's walk through the check step by step:

  1. typeof v === "object" rules out numbers, strings and other primitives.
  2. v !== null is needed separately: in JavaScript typeof null is "object" too.
  3. "name" in v checks that the field exists, and after it tsc lets you read v.name.
  4. typeof v.name === "string" checks the field's type, and the last two checks do the same for age.
  5. Inside if (isUser(data)), data has the type User, and the program prints ANNA.

Unlike an annotation, this check stays in the JavaScript and runs every time the program does. If the server sends a different object, the program goes into the else branch instead of crashing.

A server's reply

A server's reply works the same way. The method response.json() on a fetch response returns Promise<any>: tsc does not know what the server will send. So the result goes into unknown and gets checked:

  • const data: unknown = await response.json();
  • if (isUser(data)) { … }

The function fetch itself is not generic: it always returns Promise<Response>, and tsc rejects the form fetch<User>(url). For large objects such checks are usually not written by hand: libraries such as Zod describe the data's shape once and check a value against it at run time.

Interviewers like to ask why TypeScript cannot check a server's reply. The answer: types are erased at compile time, and the reply arrives at run time, when the types are gone, so code has to check it.

Here the customs picture stops working. You can post a real customs officer inside the country too, but tsc never gets inside the running program. At run time only your own code, such as isUser, checks the data.

Common mistake

The check forgets about null:

TypeScript
type User = { name: string };

function isUser(v: unknown): v is User {
    return typeof v === "object" && "name" in v // ✗
        && typeof v.name === "string";
}
TEXT
error TS18047: 'v' is possibly 'null'.

tsc reminds you that typeof null === "object". After the first check v may still be null, and "name" in null would throw a TypeError at run time. Add v !== null right after typeof v === "object".

Key points

  • Data from outside, such as a server's reply, JSON.parse or a file, was never seen by tsc, and types do not check it.
  • The annotation const user: User = JSON.parse(text) checks nothing: JSON.parse returns any.
  • Data from outside goes into unknown and is checked by a function with the predicate v is User, which does its work while the program runs.
  • response.json() returns Promise<any>, and fetch is not generic, so only your code checks a server's reply.