Skip to Content
unknown First, Then Narrow: Parse an API Payload
Intermediate
0 likes
Tutorials

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 knowUse
It is text, a number or a booleantypeof value === "string" and so on
It is a listArray.isArray(value)
It is an object with keystypeof value === "object" && value !== null
An object has a property"name" in value after the object check
It is a class instancevalue instanceof Error
It is a list of one kind of thingArray.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 every on 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.