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.parsereads; - 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:
type User = { name: string; age: number };
const user: User = JSON.parse('{"title": "report"}');
console.log(user.name.toUpperCase()); // ✗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:
const data: unknown = JSON.parse('{"title": "report"}');
console.log(data.title); // ✗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:
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:
typeof v === "object"rules out numbers, strings and other primitives.v !== nullis needed separately: in JavaScripttypeof nullis"object"too."name" in vchecks that the field exists, and after ittsclets you readv.name.typeof v.name === "string"checks the field's type, and the last two checks do the same forage.- Inside
if (isUser(data)),datahas the typeUser, and the program printsANNA.
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:
type User = { name: string };
function isUser(v: unknown): v is User {
return typeof v === "object" && "name" in v // ✗
&& typeof v.name === "string";
}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.parseor a file, was never seen bytsc, and types do not check it. - The annotation
const user: User = JSON.parse(text)checks nothing:JSON.parsereturnsany. - Data from outside goes into
unknownand is checked by a function with the predicatev is User, which does its work while the program runs. response.json()returnsPromise<any>, andfetchis not generic, so only your code checks a server's reply.