Unknown Type Guard Narrowing

Narrow unknown values safely with composable type guards, exhaustive switch on discriminants, and assertion helpers that throw precisely.

TypescriptData Integrity

Code

snippet
interface Success<T> { status: 'success'; data: T }
interface Failure { status: 'failure'; code: number; message: string }
type Outcome<T> = Success<T> | Failure;

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function hasKeys<K extends string>(
  value: unknown, ...keys: K[]
): value is Record<K, unknown> {
  return isRecord(value) && keys.every((k) => k in value);
}

function parseOutcome(raw: unknown): Outcome<string> {
  if (!hasKeys(raw, 'status')) {
    throw new TypeError('outcome requires a status field');
  }
  switch (raw.status) {
    case 'success':
      if (typeof raw.data !== 'string') {
        throw new TypeError('success outcome needs string data');
      }
      return { status: 'success', data: raw.data };
    case 'failure':
      if (!hasKeys(raw, 'code', 'message')) {
        throw new TypeError('failure outcome needs code and message');
      }
      if (typeof raw.code !== 'number' || typeof raw.message !== 'string') {
        throw new TypeError('failure fields have wrong types');
      }
      return { status: 'failure', code: raw.code, message: raw.message };
    default:
      throw new TypeError('unknown status: ' + String(raw.status));
  }
}

const ok = parseOutcome(JSON.parse('{"status":"success","data":"hello"}'));
console.log(ok.status === 'success' ? ok.data.toUpperCase() : 'n/a');