skip to content

How do you type the `ok()` and `err()` constructor helpers of a generic `Result<T, E>` union so a function can return both without annotating T and E at every call site?

level: middleimportance: should knowfreq 45%

answer

  1. only the slot the argument fixes
  2. the other slot is uninhabited
  3. never is assignable to everything
  4. ok<T>(v): Result<T, never>
  5. annotate the function, not each call

basics

~20 s

Give each helper a type parameter only for the branch it actually fills: ok<T>(value: T): Result<T, never> and err<E>(error: E): Result<never, E>. Because never is assignable to anything, both results fit the function's declared Result type.

solid answer

~50 s

Each helper should be generic **only** in the slot its argument fixes, and should put `never` in the other slot: `const ok = <T>(value: T): Result<T, never> => ({ ok: true, value })` and `const err = <E>(error: E): Result<never, E> => ({ ok: false, error })`. Since `never` is assignable to every type, `Result<number, never>` and `Result<never, RangeError>` are both assignable to a declared `Result<number, RangeError>`, so a function that returns `ok(n)` on one path and `err(new RangeError(...))` on another type-checks with no call-site annotations. The one thing you must still write is the **function's** return type — annotate it, and both branches converge on it. Without that annotation the inferred type is just the union of the two helper instantiations, which is technically usable but tells callers nothing about which errors are actually possible. Never give a helper a parameter no argument fixes; use `never` there instead.

code

typescript · 13 lines
typescript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

function parsePort(raw: string): Result<number, RangeError> {
  const n = Number(raw);
  return Number.isInteger(n) && n > 0
    ? ok(n)
    : err(new RangeError(`bad port: ${raw}`));
}

go deeper

for a junior

Know that Result is a tagged union with two type parameters — one for the success payload, one for the failure payload — and that helpers construct the two branches so callers do not write the object literals by hand.

for a middle

Explain why each helper is generic only in the slot its argument fixes, and why never in the other slot makes the value assignable to any wider Result. Be able to write both helper signatures from memory.

for a senior

Show that you annotate the enclosing function's return type so the contract states which failures are possible, and thread the error parameter through combinators like map so it is not silently dropped at the first transformation.

for a principal

Decide whether the codebase carries errors in values at all versus throwing, and set the convention: what goes in the error slot, how error unions grow across module boundaries, and where untrusted data is validated before it earns the type.

## The shape being typed A `Result` type models "success or failure" as a tagged union with two type parameters — one for the success payload, one for the failure payload: ```ts type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E }; ``` The default `E = Error` means `Result<number>` is shorthand for `Result<number, Error>`. Callers then construct values with two helpers rather than writing object literals by hand — and the whole question is how to type those helpers so inference does the work. ## The `never` trick The naive helper takes both parameters: ```ts // don't: E is fixed by no argument, so it falls back to unknown declare function err<T, E>(error: E): Result<T, E>; ``` `T` here is fixed by nothing at the call site. The compiler has no inference source for it, so it lands on `unknown` (or whatever the caller asserts), and the returned value's success branch is useless. The fix is to make each helper generic **only** in the slot its own argument determines and to put `never` in the other slot: ```ts const ok = <T>(value: T): Result<T, never> => ({ ok: true, value }); const err = <E>(error: E): Result<never, E> => ({ ok: false, error }); ``` Why `never` and not `unknown`? Because `never` is the type with no values and is assignable to *every* type, while `unknown` is assignable to almost nothing. Read `Result<number, never>` as "a success carrying a number, whose failure branch cannot occur" — that is literally true of what `ok()` returns, and it is exactly what makes the value fit anywhere a wider `Result` is expected: ```ts function parsePort(raw: string): Result<number, RangeError> { const n = Number(raw); return Number.isInteger(n) && n > 0 ? ok(n) : err(new RangeError(`bad port: ${raw}`)); } ``` Check the assignability member by member. `ok(n)` has type `Result<number, never>`, whose members are `{ ok: true; value: number }` — matching the target's success member — and `{ ok: false; error: never }`, which matches the failure member because `never` is assignable to `RangeError`. `err(...)` has type `Result<never, RangeError>` and passes for the mirror-image reason. No annotations at the call sites, no assertions. Swap `never` for `unknown` and the second half breaks immediately: `unknown` is not assignable to `RangeError`, so the helper's value no longer fits the declared return type and you are back to annotating every call. ## Annotate the function, not the calls The helpers only get you halfway. If you omit the enclosing function's return type, inference gives you the union of whatever the branches produced — a union of two *different* instantiations of `Result`. It is not wrong and it still narrows on the tag, but the signature no longer states one coherent contract, and readers of the public API cannot see which failures are possible. Writing `: Result<number, RangeError>` is the whole discipline: one declared contract, helpers that slot into it silently. When a function can fail in more than one way, widen the error slot with a union — `Result<number, RangeError | SyntaxError>` — and every `err()` call still infers on its own without annotation, because each concrete error type is assignable into the union. ## Carrying the parameters through combinators The same rule governs the operations built on top. A `map` must be generic in the input payload, the output payload, and the error, and must thread the error type through untouched: ```ts function map<T, U, E>(r: Result<T, E>, f: (value: T) => U): Result<U, E> { return r.ok ? ok(f(r.value)) : r; } ``` The failure branch returns `r` itself, already narrowed to the failure member, and the success branch returns `Result<U, never>` — assignable, again thanks to `never`. If you had written the return type as `Result<U, unknown>` or dropped `E` altogether, every caller would lose the error type at the first transformation, which is the usual way these wrappers rot. ## What is not there at runtime None of `T` and `E` survives compilation. A `Result` is a plain object with a boolean tag and one payload field; `ok()` and `err()` are two-line object constructors. The exhaustive handling that the tagged union buys you is a promise about source code the compiler has seen. Data crossing a process boundary — a parsed JSON response claiming to be a `Result` — has to be validated like any other untrusted input before it earns the type.

  • Why not type the failure helper as `err<T, E>(error: E): Result<T, E>` and let the caller pick T?
    Because no argument fixes `T`, so inference has nothing to work from and it falls back to `unknown` — or the caller supplies it by hand at every call, which is the annotation burden you were trying to avoid. Worse, an explicit type argument there lets a caller claim any success payload for a value that has none. `never` states the truth: this value's success branch cannot occur.
  • What changes when a function can fail in two different ways?
    Widen the error slot in the function's declared return type to a union, for example `Result<number, RangeError | SyntaxError>`. Each `err()` call still infers its own concrete error type and is assignable into that union with no annotation. Callers then narrow on the tag first and, if they need to distinguish failures, on the error value — usually with a tag or `instanceof` on the error itself.
  • Does any of this typing survive compilation?
    No. `Result` is a plain object with a boolean tag; the type parameters are erased along with everything else in the type layer. The compiler will not let you forget the failure branch in code it checks, but nothing enforces that at runtime, and a `Result`-shaped object arriving from the network or from untyped code must be validated before you trust its tag.

saying these in an interview costs you the question

  • Puts unknown instead of never in the unused slot
  • Adds a type parameter that no argument can fix
  • Annotates T and E by hand at every call site
  • Expects the runtime to enforce handling the failure branch
  • Drops the error type when mapping over a Result

context