Skip to Content

typia.shallow() โ€” depth-limited type check

signature
function shallow<T, N extends number = 2>(input: T): input is T; function shallow<T, N extends number = 2>(input: unknown): input is T;

typia.shallow<T, N> is is with a depth limit. It checks input against type T exactly like is does, but only descends N levels into nested objects, arrays, and tuples. Past that depth a value is accepted as long as it is a non-null object โ€” the deep leaves are not inspected.

It is meant for discrimination rather than full validation. When you only need to know which branch of a union an input belongs to, a deep structural check of every leaf is wasteful: the discriminating information usually lives in a key near the surface.

ScenarioPick
Full structural guaranteeis
Just pick the union branch / discriminator, cheaplyshallow (you are here)
One field is wrong โ†’ throw an exceptionassert
Need every field error at oncevalidate

First example

Only the discriminator and the top-level shape are checked, so the deeply nested fields never enter the emitted guard.

examples/src/validators/shallow.ts
import typia from "typia"; interface Circle { type: "circle"; radius: number; } const input: unknown = { type: "circle", radius: 5, }; // only the discriminator and the top-level shape are checked if (typia.shallow<Circle, 1>(input)) { console.log(input.radius); // 5 }

What N does

N is a non-negative integer literal (default 2). Each level of nesting into an object, array element, or tuple member spends one unit of budget. Once the budget reaches 0, a composite value is accepted with a bare typeof input === "object" && input !== null guard instead of being descended.

examples/src/validators/shallow-depth.ts
import typia from "typia"; interface Deep { kind: "deep"; a: { b: { c: { value: string } } }; } const input: unknown = { kind: "deep", a: { b: { c: { value: "hello" } } }, }; typia.shallow<Deep, 0>(input); // typeof input === "object" && input !== null typia.shallow<Deep, 1>(input); // also checks `kind` and that `a` is an object typia.shallow<Deep, 3>(input); // descends to `a.b.c`, stops before `value` typia.is<Deep>(input); // full descent, including `a.b.c.value`

Because the budget is finite, shallow also terminates on recursive types โ€” a self-referential type (e.g. interface Tree { value: string; children: Tree[] }) stops descending once the budget runs out, so typia.shallow<Tree, 2> inlines two levels and then accepts the rest structurally.

Reusable checkers

Like createIs, you can hoist a shallow checker so the type-specific code is emitted once and reused. typia.createShallow<T, N>() takes no arguments and returns a plain (input: unknown) => input is T function carrying the same depth bound.

examples/src/validators/createShallow.ts
import typia from "typia"; interface Circle { type: "circle"; radius: number; } interface Square { type: "square"; side: number; } type Shape = Circle | Square; export const isShape = typia.createShallow<Shape, 1>();

How it compares to is

typia.shallow<T, N> emits the exact same checks as typia.is<T> for everything within depth N. The only difference is what happens past the budget: is keeps descending into every leaf, while shallow accepts a non-null object. So the emitted code for shallow is a strict prefix of the is code โ€” never more work, and usually much less for deeply nested or recursive types.

This makes shallow a good fit for hot paths where you only branch on a discriminator, and is the right pick whenever you actually consume the deep fields afterward.

Where to go next

  • Need the full structural guarantee โ†’ is
  • Need an error path or to throw โ†’ assert
  • Need every error at once โ†’ validate
Last updated on