unknown First, Then Narrow: Parse an API Payload
Receive a nested payload as unknown and narrow it with two small helpers until every property is checked. After this, you can write a parser that rejects wrong data and returns a real type.
An API response is the clearest case for unknown. You did not write the data, you cannot see it at compile time, and the next release of the service can change it. This page takes one nested payload and narrows it step by step, so every property you read has been checked. It builds on the course lesson about unknown and any, but it is a different payload, with a list inside an object.
Step 1: receive it as unknown
const text = '{"user":{"id":7,"name":"Ana","tags":["admin","beta"]}}';
const payload: unknown = JSON.parse(text);
console.log(payload.user.name);
start.ts(4,13): error TS18046: 'payload' is of type 'unknown'.
The compiler will not let you read user from something it knows nothing about. Good. Every step below removes some of that doubt.
Step 2: two small helpers
Most payload checks come down to two questions: is it an object with keys, and is it a list of strings.
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === "string");
}
isRecord rejects null and arrays, which both have typeof equal to "object". After it returns true, you can read keys, and every value is still unknown. isStringArray checks every item, not just the first.
Step 3: walk down one level at a time
type User = { id: number; name: string; tags: string[] };
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === "string");
}
function parseUser(payload: unknown): User | null {
if (!isRecord(payload) || !isRecord(payload.user)) {
return null;
}
const { id, name, tags } = payload.user;
if (typeof id !== "number" || typeof name !== "string" || !isStringArray(tags)) {
return null;
}
return { id, name, tags };
}
const good = '{"user":{"id":7,"name":"Ana","tags":["admin","beta"]}}';
const wrongTags = '{"user":{"id":7,"name":"Ana","tags":["admin",3]}}';
const noUser = '{"account":{"id":7}}';
for (const text of [good, wrongTags, noUser]) {
const user = parseUser(JSON.parse(text));
console.log(user === null ? "rejected" : `${user.name} has ${user.tags.length} tags`);
}
Ana has 2 tags
rejected
rejected
Read parseUser from the top. The first if makes sure the payload is an object and that its user property is an object too. The destructuring pulls out three values, each still unknown. The second if checks all three. Only then does the function build a User. The return type User | null makes the caller deal with a rejected payload.
Step 4: which tool for which check
| You want to know | Use |
|---|---|
| It is text, a number or a boolean | typeof value === "string" and so on |
| It is a list | Array.isArray(value) |
| It is an object with keys | typeof value === "object" && value !== null |
| An object has a property | "name" in value after the object check |
| It is a class instance | value instanceof Error |
| It is a list of one kind of thing | Array.isArray(value) && value.every(...) |
⚠ Warning: Do not stop at typeof value === "object". null passes it, and so does an array. Both lines of the table that mention objects need the null check.
Common mistakes
- Casting the payload to your type after checking one field.
- Using
everyon something you have not checked is an array first. - Returning the parsed object itself. Build a new one from the checked fields so unexpected extras are dropped.
Related course: TypeScript Intermediate.
About
Receive a nested payload as unknown and narrow it with two small helpers until every property is checked. After this, you can write a parser that rejects wrong data and returns a real type.
- Responsible
- John G
- Last Update
- 10/21/2026
- Reading Time
- 3 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.