| |

TypeScript 70 🔷 Typing JSON and Unknown Data

Every application receives data it did not create. The JSON from an API, the body of a POST request, the contents of a config file, the response from a third-party service, the message from a WebSocket — all of it arrives as an untyped blob, and TypeScript cannot know its shape without being told. The JSON.parse returns any, and every property access on it is unchecked. The unknown type is the correct alternative, and the validation libraries — Zod, Valibot, io-ts, ArkType — turn the runtime check into the compile-time type. This chapter covers the full workflow: the any problem, the unknown alternative, the type guards, the schema validation, the satisfies operator, the as const for the literal data, the JsonValue type, the parsing’s errors, and the patterns that make the untrusted data safe. It builds on the validation material from TypeScript 54 and the error handling from TypeScript 68.

Key point: The JSON.parse returns any, which disables the type checking for everything downstream. The fix is to change the return type to unknown and validate. The unknown is the top type — it accepts any value but requires narrowing before use. The type guards narrow the unknown to the specific type, and the schema validation libraries generate both the guard and the type from a single schema. The satisfies operator checks a value against a type without widening it, which preserves the literal types. The JsonValue type models the JSON’s recursive structure. The parsing’s failures are the SyntaxError, and the validation’s failures are the schema library’s errors.


The any problem

JSON.parse returns any, and any disables the type system. Every property access, every method call, every assignment is unchecked.

const data = JSON.parse('{"name":"Alice","age":30}');
// data is any
console.log(data.name);        // no checking
console.log(data.nonExistent); // no error, undefined at runtime
data.foo();                    // no error, TypeError at runtime

The data is any, and the compiler accepts everything. The data.nonExistent is the undefined at runtime, and the data.foo() is the TypeError. The two are the runtime failures the compiler did not catch.

Why the any is the problem. The any is the escape hatch, and the escape is the danger. The any disables the checking, and the checking is the safety. The any is the legacy, and the legacy is the risk.

Why the JSON.parse returns any. The function’s signature is the parse(text: string, reviver?: ...): any, and the any is the legacy. The modern libraries override the type, and the override is the fix. The any is the default, and the default is the problem.

Why the as cast is not the fix. The JSON.parse(text) as User is the assertion, and the assertion is the claim. The compiler trusts the claim, and the runtime may not. The cast is the lie, and the lie is the danger.

const user = JSON.parse(text) as User;
// The compiler trusts the cast.
// The runtime does not verify it.

Why the runtime’s value matters. The JSON is the untrusted, and the untrusted is the runtime’s. The schema is the validation, and the validation is the safety. The two are the pair, and the pair is the modern.

Why the parsing’s errors are separate. The JSON.parse throws the SyntaxError on the invalid JSON, and the schema throws the ZodError on the invalid shape. The two are the different, and the two are the separate. The parsing is the syntax’s, and the validation is the shape’s.

Why the pattern should be the layered. The parse, the validate, the type. The three are the layers, and the layers are the safety. The JSON.parse is the first, the schema is the second, and the type is the third. The three are the pattern, and the pattern is the modern.

Why the any should be the avoided. The any is the escape, and the escape is the risk. The unknown is the correct, and the correct is the safety. The two are the choice, and the choice is the design.

Why the unknown is the correct default. The unknown is the top type, and the top accepts the any. The unknown requires the narrowing, and the narrowing is the safety. The any is the unsafe, and the unsafe is the legacy. The unknown is the modern, and the modern is the correct.


The unknown alternative

The unknown is the correct type for the untyped data. It accepts the any value, but it requires the narrowing before the use.

const data: unknown = JSON.parse('{"name":"Alice"}');
// data is unknown
// console.log(data.name);  // ❌ error: data is unknown

The data.name is the error, and the error forces the narrowing. The narrowing is the safety, and the safety is the value.

Why the unknown requires the narrowing. The unknown accepts the any, and the any has no shape. The narrowing is the shape’s check, and the check is the safety. The unknown is the top, and the top is the safe.

The typeof guard. The typeof guard narrows the primitives.

function processData(data: unknown): void {
  if (typeof data === 'string') {
    console.log(data.toUpperCase());  // data is string
  } else if (typeof data === 'number') {
    console.log(data.toFixed(2));     // data is number
  } else if (typeof data === 'object' && data !== null) {
    console.log(Object.keys(data));   // data is object
  }
}

The typeof narrows to the string, the number, and the object. The data !== null is the requirement, because the typeof null is the 'object'. The two are the pair, and the pair is the safe.

Why the typeof null is the 'object'. The JavaScript’s legacy, and the null is the object’s type. The data !== null is the guard, and the guard is the safety. The two are the pair, and the pair is the requirement.

The in guard. The in guard narrows the object’s properties.

function processUser(data: unknown): void {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    // data is object & Record<'name', unknown>
    console.log(data.name);
  }
}

The 'name' in data narrows the data to the object with the name. The data.name is the access, and the access is the narrowed.

The instanceof guard. The instanceof narrows the class’s instances.

function processError(data: unknown): void {
  if (data instanceof Error) {
    console.log(data.message);  // data is Error
  }
}

The data instanceof Error narrows the data to the Error, and the message is the access. The instanceof is the class’s, and the class’s is the specific.

The array’s guard. The Array.isArray narrows the arrays.

function processArray(data: unknown): void {
  if (Array.isArray(data)) {
    // data is any[]
    console.log(data.length);
  }
}

The Array.isArray(data) narrows the data to the array, and the length is the access. The Array.isArray is the built-in, and the built-in is the reliable.

Why the array’s guard is the any[]. The Array.isArray narrows to the any[], and the elements are the any. The elements’ narrowing is the additional, and the additional is the loop. The array.every is the pattern, and the pattern is the check.

Why the guards are the manual. The guards are the manual, and the manual is the verbose. The schema validation is the automatic, and the automatic is the modern. The two are the choice, and the choice is the scale.


The schema validation

The schema validation libraries generate both the guard and the type from a single schema. The Zod is the common, and the Valibot, the io-ts, and the ArkType are the alternatives.

The Zod’s schema. The z.object({...}) defines the shape, and the z.infer extracts the type.

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
});

type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; age?: number }

The UserSchema is the schema, and the User is the inferred type. The two are the same, and the same is the single source.

Why the single source matters. The schema is the runtime’s, and the type is the compile-time’s. The two are the single source, and the single source is the sync. The drift is the problem, and the sync is the fix.

The safeParse. The safeParse returns the { success: true; data: T } or the { success: false; error: ZodError }.

const result = UserSchema.safeParse(jsonData);
if (result.success) {
  const user: User = result.data;
  console.log(user.name);
} else {
  console.error(result.error.issues);
}

The result.success is the discriminant, and the result.data and the result.error are the payloads. The safeParse is the safe, and the parse is the throwing.

Why the safeParse is the safe. The safeParse returns the result, and the result is the discriminated union. The parse throws, and the throw is the exception. The two are the choice, and the choice is the style.

The parse. The parse returns the data or throws the ZodError.

try {
  const user = UserSchema.parse(jsonData);
  console.log(user.name);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error(error.issues);
  }
}

The parse throws the ZodError, and the catch narrows it. The parse is the concise, and the concise is the common.

Why the ZodError is the specific. The ZodError has the issues, and the issues is the array. The issues has the path and the message, and the two are the diagnosis. The ZodError is the specific, and the specific is the check.

The safeParse‘s result’s narrow. The result.success narrows the result to the data or the error.

const result = UserSchema.safeParse(data);
const message = result.success
  ? `User: ${result.data.name}`
  : `Error: ${result.error.issues[0].message}`;

The ternary narrows the result, and the two branches are the access. The result.data and the result.error are the narrowed, and the narrowed is the safe.

Why the validation should be the boundary. The validation is the boundary’s, and the boundary is the entry’s. The API’s response, the request’s body, the file’s contents are the boundaries, and the boundaries are the validation’s. The interior is the trusted, and the trusted is the typed.

Why the validation should be the once. The validation is the once, and the once is the performance. The re-validation is the waste, and the waste is the cost. The type is the trust, and the trust is the interior’s.


The satisfies operator

The satisfies operator checks a value against a type without widening it, which preserves the literal types.

const config = {
  host: 'localhost',
  port: 8080,
} satisfies Config;

// config.host is 'localhost', not string
// config.port is 8080, not number

The config is checked against the Config, and the literal types are preserved. The config.host is the 'localhost', and the config.port is the 8080. The satisfies is the check, and the check is the literal’s.

Why the satisfies matters. The satisfies is the check, and the check is the literal’s. The : Config annotation widens the literal, and the satisfies does not. The two are the difference, and the difference is the literal.

The satisfies‘s use with the config. The config’s object is the satisfies‘s common, and the literal’s preservation is the value.

type Config = {
  host: string;
  port: number;
  protocol?: 'http' | 'https';
};

const config = {
  host: 'localhost',
  port: 8080,
  protocol: 'https',
} satisfies Config;
// config.protocol is 'https', not 'http' | 'https'

The config.protocol is the 'https', and the literal is the preserved. The satisfies is the check, and the check is the literal’s.

Why the satisfies and the as const differ. The as const makes the value the readonly and the literal, and the satisfies checks the value against the type. The two are the different, and the different is the use. The as const is the value’s, and the satisfies is the check’s.

The satisfies with the as const. The two can be combined, and the combination is the readonly and the checked.

const config = {
  host: 'localhost',
  port: 8080,
} as const satisfies Config;
// config is readonly, and the literal types are preserved.

The as const satisfies Config is the readonly and the checked. The config.host is the 'localhost', and the config is the readonly. The two are the combination, and the combination is the modern.

Why the satisfies matters for the JSON. The JSON’s literal data is the satisfies‘s, and the literal’s preservation is the value. The satisfies is the check, and the check is the safety.

Why the satisfies should be the preference. The satisfies is the modern, and the modern is the preference. The : Type annotation is the widening, and the widening is the loss. The satisfies is the check, and the check is the preservation.

Why the satisfies is the TypeScript 4.9‘s. The satisfies is the 4.9‘s, and the 4.9 is the modern. The older versions do not have it, and the older versions use the as const and the annotation. The satisfies is the modern, and the modern is the upgrade.


The JsonValue type

The JsonValue is the recursive type that models the JSON’s structure. The string, the number, the boolean, the null, the array, and the object are the six.

type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

The JsonValue is the recursive, and the recursion is the array and the object. The JSON.parse‘s result is the JsonValue, and the JsonValue is the typed.

Why the JsonValue is the recursive. The JSON’s structure is the recursive, and the recursion is the array and the object. The JsonValue[] is the array, and the { [key: string]: JsonValue } is the object. The two are the recursive, and the recursive is the model.

The JSON.parse with the JsonValue. The JSON.parse returns the JsonValue, and the JsonValue is the typed.

function parseJson(text: string): JsonValue {
  return JSON.parse(text) as JsonValue;
}

The parseJson returns the JsonValue, and the JsonValue is the model. The as JsonValue is the assertion, and the assertion is the safe (because the JsonValue is the top of the JSON).

Why the as JsonValue is the safe. The JSON.parse returns the any, and the as JsonValue narrows the any to the JsonValue. The JsonValue is the top, and the top is the safe. The assertion is the safe, and the safe is the value.

The JsonValue‘s narrowing. The JsonValue is the union, and the narrowing is the typeof.

function processJson(value: JsonValue): void {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else if (Array.isArray(value)) {
    console.log(value.length);
  } else if (value !== null && typeof value === 'object') {
    console.log(Object.keys(value));
  }
}

The typeof narrows the string, the number, and the boolean, and the Array.isArray narrows the array, and the value !== null && typeof value === 'object' narrows the object. The four are the branches, and the branches are the narrowing.

Why the JsonValue‘s narrowing matters. The JsonValue‘s narrowing is the access, and the access is the safety. The value.toUpperCase() is the string’s, and the value.length is the array’s. The two are the narrowed, and the narrowed is the safe.

Why the JsonValue is the general. The JsonValue is the general, and the general is the loose. The specific type is the schema’s, and the schema is the specific. The two are the different, and the different is the use. The JsonValue is the general’s, and the schema is the specific’s.

Why the JsonValue can be the intermediate. The JsonValue can be the intermediate, and the intermediate is the transition. The JSON.parse returns the JsonValue, and the schema validates the JsonValue to the specific. The two are the steps, and the steps are the pattern.

Why the JsonValue should be the first. The JsonValue is the first, and the first is the parse’s. The JSON.parse is the first, and the JsonValue is the result. The schema is the second, and the second is the validation’s. The two are the steps, and the steps are the pattern.


The parsing’s errors

The parsing’s errors are the SyntaxError, and the validation’s errors are the schema’s. The two are the different, and the two are the separate.

The SyntaxError. The JSON.parse throws the SyntaxError on the invalid JSON.

try {
  const data = JSON.parse('{invalid json}');
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error('Invalid JSON:', error.message);
  }
}

The error instanceof SyntaxError narrows the error to the SyntaxError, and the message is the access. The SyntaxError is the parse’s, and the parse’s is the syntax’s.

Why the SyntaxError is the specific. The SyntaxError is the specific, and the specific is the parse’s. The JSON.parse throws the SyntaxError, and the catch narrows it. The two are the pair, and the pair is the safe.

The validation’s error. The schema’s error is the ZodError (or the library’s), and the issues is the array.

const result = UserSchema.safeParse(data);
if (!result.success) {
  for (const issue of result.error.issues) {
    console.error(`${issue.path.join('.')}: ${issue.message}`);
  }
}

The result.error.issues is the array, and the issue.path and the issue.message are the diagnosis. The ZodError is the validation’s, and the validation’s is the shape’s.

Why the validation’s error is the specific. The validation’s error is the specific, and the specific is the shape’s. The ZodError has the issues, and the issues is the diagnosis. The two are the pair, and the pair is the report.

Why the two errors are separate. The parsing’s and the validation’s are the separate, and the separate is the layer. The parse is the syntax’s, and the validate is the shape’s. The two are the layers, and the layers are the pattern.

Why the two errors should be the both handled. The both handled is the complete, and the complete is the safety. The parse’s SyntaxError and the validation’s ZodError are the both, and the both is the handling. The two are the pattern, and the pattern is the modern.

Why the errors’ order matters. The parse is the first, and the validate is the second. The parse’s error precedes the validate’s, and the order is the sequence. The two are the steps, and the steps are the pattern.


The JSON’s serialization

The JSON.stringify serializes the value to the string, and the toJSON method customizes the serialization.

const user = { id: '1', name: 'Alice' };
const json = JSON.stringify(user);
// '{"id":"1","name":"Alice"}'

The stringify returns the string, and the string is the JSON. The user is the object, and the json is the string.

Why the JSON.stringify is the reverse. The stringify is the parse‘s reverse, and the reverse is the pair. The parse is the string to the object, and the stringify is the object to the string. The two are the pair, and the pair is the JSON.

The toJSON method. The toJSON method customizes the serialization.

class User {
  constructor(readonly id: string, readonly name: string) {}
  toJSON(): { id: string; name: string } {
    return { id: this.id, name: this.name };
  }
}

const user = new User('1', 'Alice');
const json = JSON.stringify(user);
// '{"id":"1","name":"Alice"}'

The toJSON is the custom, and the custom is the control. The stringify calls the toJSON, and the toJSON returns the object. The two are the pair, and the pair is the serialization.

Why the toJSON matters. The toJSON is the custom, and the custom is the control. The class’s serialization is the toJSON‘s, and the toJSON is the pattern. The two are the pair, and the pair is the class.

The replacer and the reviver. The stringify‘s replacer and the parse‘s reviver customize the serialization.

const json = JSON.stringify(data, (key, value) => {
  if (key === 'password') return undefined;
  return value;
});

const data = JSON.parse(json, (key, value) => {
  if (key === 'date') return new Date(value);
  return value;
});

The replacer is the filter, and the reviver is the transform. The two are the custom, and the custom is the control.

Why the replacer matters. The replacer is the filter, and the filter is the security. The password’s removal is the example, and the removal is the safety. The two are the pair, and the pair is the security.

Why the reviver matters. The reviver is the transform, and the transform is the convenience. The date’s conversion is the example, and the conversion is the pattern. The two are the pair, and the pair is the convenience.

Why the serialization should be the typed. The serialization is the typed, and the typed is the safety. The toJSON‘s return is the typed, and the typed is the contract. The two are the pair, and the pair is the safety.


The validation’s libraries

The validation’s libraries are the Zod, the Valibot, the io-ts, the ArkType, the Ajv, and the rest. The Zod is the common, and the Valibot is the modern’s lightweight.

The Zod. The Zod is the common, and the chainable is the API.

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
});

The Zod is the chainable, and the chainable is the ergonomic. The z.object, the z.string, and the z.number are the primitives, and the primitives are the common.

The Valibot. The Valibot is the lightweight, and the functional is the API.

import * as v from 'valibot';

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
  age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
});

The Valibot is the functional, and the functional is the composition. The v.pipe is the composition, and the composition is the pattern. The Valibot is the lightweight, and the lightweight is the modern.

The io-ts. The io-ts is the functional’s, and the Either is the result.

import * as t from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';

const UserCodec = t.type({
  id: t.string,
  name: t.string,
  email: t.string,
});

const result = UserCodec.decode(data);
if (result._tag === 'Left') {
  console.log(PathReporter.report(result));
}

The io-ts is the codec’s, and the codec is the functional’s. The decode returns the Either, and the Either is the result. The io-ts is the functional’s, and the functional’s is the choice.

Why the libraries’ choice matters. The libraries are the different, and the different is the choice. The Zod is the common, the Valibot is the lightweight, and the io-ts is the functional’s. The team’s choice is the consistency, and the consistency is the value.

Why the schema should be the single source. The schema is the runtime’s, and the type is the compile-time’s. The single source is the sync, and the sync is the value. The two are the pair, and the pair is the pattern.

Why the validation should be the boundary. The validation is the boundary’s, and the boundary is the entry’s. The API’s response, the request’s body, the file’s contents are the boundaries, and the boundaries are the validation’s. The interior is the trusted, and the trusted is the typed.

Why the validation should be the fail-fast. The validation is the fail-fast, and the fail-fast is the early. The error’s early is the diagnosis, and the diagnosis is the value. The two are the pair, and the pair is the safety.


Complete Example Session

// ============================================
// PART 1: THE ANY PROBLEM
// ============================================

const data = JSON.parse('{"name":"Alice"}');
// data is any
console.log(data.name);         // no checking
console.log(data.nonExistent);  // no error

// ============================================
// PART 2: THE UNKNOWN ALTERNATIVE
// ============================================

const safeData: unknown = JSON.parse('{"name":"Alice"}');
// safeData is unknown
// console.log(safeData.name);  // ❌ error

// ============================================
// PART 3: THE TYPE GUARDS
// ============================================

function processData(data: unknown): void {
  if (typeof data === 'string') {
    console.log(data.toUpperCase());
  } else if (typeof data === 'number') {
    console.log(data.toFixed(2));
  } else if (typeof data === 'object' && data !== null && 'name' in data) {
    console.log(data.name);
  }
}

// ============================================
// PART 4: THE ZOD SCHEMA
// ============================================

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
});

type User = z.infer<typeof UserSchema>;

// ============================================
// PART 5: THE SAFE PARSE
// ============================================

const jsonData = JSON.parse('{"id":"550e8400-e29b-41d4-a716-446655440000","name":"Alice","email":"alice@example.com"}');

const result = UserSchema.safeParse(jsonData);
if (result.success) {
  const user: User = result.data;
  console.log(user.name);
} else {
  for (const issue of result.error.issues) {
    console.error(`${issue.path.join('.')}: ${issue.message}`);
  }
}

// ============================================
// PART 6: THE SATISFIES
// ============================================

type Config = {
  host: string;
  port: number;
  protocol?: 'http' | 'https';
};

const config = {
  host: 'localhost',
  port: 8080,
  protocol: 'https',
} satisfies Config;
// config.protocol is 'https', not 'http' | 'https'

// ============================================
// PART 7: THE AS CONST SATISFIES
// ============================================

const config2 = {
  host: 'localhost',
  port: 8080,
} as const satisfies Config;
// config2 is readonly, and the literal types are preserved.

// ============================================
// PART 8: THE JSON VALUE
// ============================================

type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

function parseJson(text: string): JsonValue {
  return JSON.parse(text) as JsonValue;
}

function processJson(value: JsonValue): void {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else if (Array.isArray(value)) {
    console.log(value.length);
  } else if (value !== null && typeof value === 'object') {
    console.log(Object.keys(value));
  }
}

// ============================================
// PART 9: THE ERRORS
// ============================================

try {
  const data = JSON.parse('{invalid}');
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error('Invalid JSON:', error.message);
  }
}

const validationResult = UserSchema.safeParse(jsonData);
if (!validationResult.success) {
  console.error(validationResult.error.issues);
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't use the any
const bad = JSON.parse(text);  // the any

// Don't cast without the validation
const user = JSON.parse(text) as User;  // the cast is the lie

// Don't forget the runtime's check
// The `as` is the compile-time's, not the runtime's.

// Don't skip the array's check
// The `Array.isArray` is the requirement.

// Don't forget the `null`'s check
// The `typeof null` is the `'object'`.

// Don't use the schema for the non-boundary
// The interior is the trusted.

// Don't forget the serialization's typing
// The `toJSON`'s return is the contract.

The ten parts cover the any problem, the unknown alternative, the type guards, the Zod schema, the safeParse, the satisfies, the as const satisfies, the JsonValue, the errors, and the anti-patterns.


Quick Reference

The Types

The typeThe purpose
The anyThe unsafe
The unknownThe safe
The JsonValueThe JSON’s model
The z.inferThe schema’s type

The Type Guards

The guardThe narrow
The typeofThe primitives
The instanceofThe class
The Array.isArrayThe array
The 'key' in objThe object’s property
The data !== nullThe non-null

The Zod’s API

The methodThe purpose
z.object({...})The object’s schema
z.string()The string
.min(), .max()The constraints
.email(), .uuid()The formats
.optional()The optional
.safeParse()The safe’s parse
.parse()The throwing’s parse
.infer<typeof S>The type

The satisfies and the as const

The operatorThe purpose
The : TypeThe widening
The satisfies TypeThe check, the preservation
The as constThe readonly, the literal
The as const satisfies TypeThe both

The Errors

The errorThe source
The SyntaxErrorThe JSON.parse
The ZodErrorThe Zod’s parse
The result.error.issuesThe Zod’s safeParse

The Libraries

The libraryThe style
The ZodThe chainable
The ValibotThe functional, the lightweight
The io-tsThe functional, the codec
The ArkTypeThe fast
The AjvThe JSON Schema

Best Practices

✅ Do This:

// Use the unknown for the untyped
const data: unknown = JSON.parse(text);                        // ✅

// Use the type guards
if (typeof data === 'object' && data !== null && 'name' in data) { ... } // ✅

// Use the Zod's safeParse
const result = UserSchema.safeParse(data);
if (result.success) { const user: User = result.data; }        // ✅

// Use the satisfies for the literals
const config = { host: 'localhost' } satisfies Config;         // ✅

// Use the as const satisfies
const config = { host: 'localhost' } as const satisfies Config;// ✅

// Use the JsonValue for the JSON
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; // ✅

// Validate at the boundary
const user = UserSchema.parse(response.json());                // ✅

// Use the toJSON for the serialization
class User { toJSON(): { id: string; name: string } { ... } }  // ✅

❌ Don’t Do This:

// Don't use the any
const data = JSON.parse(text);  // the any                     // ⚠️

// Don't cast without the validation
const user = JSON.parse(text) as User;  // the lie             // ⚠️

// Don't forget the runtime's check
// The `as` is the compile-time's.                             // ⚠️

// Don't skip the array's check
// The `Array.isArray` is the requirement.                    // ⚠️

// Don't forget the null's check
// The `typeof null` is the 'object'.                         // ⚠️

// Don't use the schema for the non-boundary
// The interior is the trusted.                               // ⚠️

// Don't forget the serialization's typing
// The toJSON's return is the contract.                       // ⚠️

Common Pitfalls

PitfallProblemSolution
The anyThe uncheckedThe unknown
The cast without the validationThe lieThe schema
The typeof nullThe ‘object’The !== null
The array’s missing checkThe wrong typeThe Array.isArray
The schema’s everywhereThe performanceThe boundary
The satisfies‘s wideningThe literal’s lossThe satisfies
The as const‘s missingThe widenedThe as const
The serialization’s untypedThe contract’s lossThe toJSON

Real-World Examples

1. The unknown

const data: unknown = JSON.parse(text);

2. The type guard

if (typeof data === 'object' && data !== null && 'name' in data) {
  console.log(data.name);
}

3. The Zod schema

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

4. The safeParse

const result = UserSchema.safeParse(data);
if (result.success) console.log(result.data);

5. The satisfies

const config = { host: 'localhost' } satisfies Config;

6. The as const satisfies

const config = { host: 'localhost' } as const satisfies Config;

7. The JsonValue

type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };

8. The SyntaxError

catch (error) {
  if (error instanceof SyntaxError) console.error(error.message);
}

9. The ZodError

if (!result.success) console.error(result.error.issues);

10. The toJSON

class User {
  toJSON(): { id: string; name: string } {
    return { id: this.id, name: this.name };
  }
}

Visual: The Workflow

┌──────────────────────────────────────────────────────────┐
│  THE UNTRUSTED INPUT                                     │
│    The API's response, the request's body, the file      │
│       │                                                  │
│       ▼                                                  │
│  THE PARSE (JSON.parse)                                  │
│    The string → the any                                  │
│       │                                                  │
│       ▼                                                  │
│  THE VALIDATION (the schema)                             │
│    The any → the specific or the error                   │
│       │                                                  │
│       ▼                                                  │
│  THE TYPED VALUE                                         │
│    The specific type, the trusted                        │
│       │                                                  │
│       ▼                                                  │
│  THE INTERIOR                                            │
│    The trusted, the typed                                │
│                                                          │
│  The validation is the boundary's, and the interior is   │
│  the trusted.                                            │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The unknown‘s Narrowing

┌──────────────────────────────────────────────────────────┐
│  const data: unknown = JSON.parse(text);                 │
│                                                          │
│  THE GUARDS                                              │
│    typeof data === 'string'  → string                    │
│    typeof data === 'number'  → number                    │
│    typeof data === 'boolean' → boolean                   │
│    typeof data === 'object' && data !== null → object    │
│    Array.isArray(data)       → any[]                     │
│    data instanceof Error     → Error                     │
│    'key' in data             → object & Record<'key', unknown>│
│                                                          │
│  The narrowing is the safety.                            │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Zod

┌──────────────────────────────────────────────────────────┐
│  const UserSchema = z.object({                           │
│    id: z.string().uuid(),                                │
│    name: z.string().min(1),                              │
│    email: z.string().email(),                            │
│    age: z.number().int().min(0).optional(),              │
│  });                                                     │
│                                                          │
│  type User = z.infer<typeof UserSchema>;                 │
│    → { id: string; name: string; email: string; age?: number }│
│                                                          │
│  THE SINGLE SOURCE                                       │
│    The schema is the runtime's, and the type is the      │
│    compile-time's. The two are the sync.                 │
│                                                          │
│  THE PARSE                                               │
│    UserSchema.parse(data)  → the User or the ZodError    │
│    UserSchema.safeParse(data)  → the { success, data/error }│
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The satisfies

┌──────────────────────────────────────────────────────────┐
│  type Config = { host: string; port: number };           │
│                                                          │
│  const config = {                                        │
│    host: 'localhost',                                    │
│    port: 8080,                                           │
│  } satisfies Config;                                     │
│                                                          │
│  config.host  → 'localhost' (the literal)                │
│  config.port  → 8080 (the literal)                       │
│                                                          │
│  THE COMPARISON                                          │
│    const config: Config = { ... }                        │
│      → config.host is string (the widened)               │
│    const config = { ... } satisfies Config               │
│      → config.host is 'localhost' (the preserved)        │
│                                                          │
│  The satisfies preserves the literal types.              │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The JsonValue

┌──────────────────────────────────────────────────────────┐
│  type JsonValue =                                        │
│    | string                                              │
│    | number                                              │
│    | boolean                                             │
│    | null                                                │
│    | JsonValue[]                                         │
│    | { [key: string]: JsonValue };                       │
│                                                          │
│  THE RECURSION                                           │
│    The array and the object are the recursive.           │
│                                                          │
│  THE NARROWING                                           │
│    typeof value === 'string'  → string                   │
│    typeof value === 'number'  → number                   │
│    typeof value === 'boolean' → boolean                  │
│    value === null             → null                     │
│    Array.isArray(value)       → JsonValue[]              │
│    typeof value === 'object'  → { [key: string]: JsonValue }│
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Errors

┌──────────────────────────────────────────────────────────┐
│  THE PARSE'S ERROR                                       │
│    JSON.parse('{invalid}')                               │
│      → throws the SyntaxError                            │
│                                                          │
│  THE VALIDATION'S ERROR                                  │
│    UserSchema.safeParse(data)                            │
│      → the { success: false, error: ZodError }           │
│    UserSchema.parse(data)                                │
│      → throws the ZodError                               │
│                                                          │
│  THE ZODERROR'S ISSUES                                   │
│    result.error.issues                                   │
│      → [                                                 │
│          { path: ['email'], message: 'Invalid email' },  │
│          { path: ['age'], message: 'Expected number' },  │
│        ]                                                 │
│                                                          │
│  The two errors are the separate, and the separate is    │
│  the layer.                                              │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
The anyThe unsafe
The unknownThe safe
The type guardsThe typeof, the instanceof, the Array.isArray, the in
The schemaThe Zod, the Valibot, the io-ts
The safeParseThe non-throwing
The parseThe throwing
The z.inferThe type’s extraction
The satisfiesThe check, the literal’s preservation
The as constThe readonly, the literal
The JsonValueThe JSON’s model
The SyntaxErrorThe parse’s
The ZodErrorThe validation’s

Key takeaways:

  • The JSON.parse returns any, which disables the type checking — the unknown is the correct alternative, and the narrowing is the safety
  • The unknown requires the narrowing before the use — the typeof, the instanceof, the Array.isArray, and the in are the guards
  • The schema validation libraries generate both the guard and the type — the z.infer extracts the type, and the safeParse is the non-throwing
  • The satisfies operator checks a value against a type without widening it — the literal types are preserved, and the : Type annotation widens them
  • The as const satisfies combines the readonly and the checked — the two are the modern, and the modern is the preference
  • The JsonValue is the recursive type that models the JSON’s structure — the string, the number, the boolean, the null, the array, and the object are the six
  • The parse’s SyntaxError and the validation’s ZodError are the separate — the parse is the syntax’s, and the validation is the shape’s
  • The validation should be the boundary’s, and the once — the API’s response, the request’s body, and the file’s contents are the boundaries, and the interior is the trusted
  • The toJSON method customizes the serialization — the class’s serialization is the toJSON‘s, and the return is the contract
  • The unknown is the correct default for the untrusted data — the any is the legacy, and the legacy is the risk

Remember: The JSON and the unknown data are the untrusted, and the untrusted is the runtime’s. The JSON.parse returns the any, and the any is the unsafe. The unknown is the correct, and the schema is the validation. The satisfies preserves the literals, and the as const makes them readonly. The JsonValue models the JSON, and the SyntaxError and the ZodError are the separate. The validation is the boundary’s, and the boundary is the safety. The types are the compiler’s, and the validation is the runtime’s.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!