Unknown Type Guard Narrowing
Narrow unknown values safely with composable type guards, exhaustive switch on discriminants, and assertion helpers that throw precisely.
Code
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');Report a problem with this exampleOpens a pre-filled issue on GitHub.