typia.shallow() โ depth-limited type check
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.
| Scenario | Pick |
|---|---|
| Full structural guarantee | is |
| Just pick the union branch / discriminator, cheaply | shallow (you are here) |
| One field is wrong โ throw an exception | assert |
| Need every field error at once | validate |
First example
Only the discriminator and the top-level shape are checked, so the deeply nested fields never enter the emitted guard.
TypeScript Source
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.
TypeScript Source
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.
TypeScript Source
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.