Mesrai
Back to blog
// essayTechnical Deep Dive

Mistyped Generic Constraint: How Mesrai Reads the Inference Path

Real PR catch: pluck with widened input — constraint degrades to any. Parse-at-boundary fix.

Mesrai TeamAugust 13, 20268 min read

A `pluck` helper extracting a key from objects. Generic over both the element type and the key. Looked right. Callers passed an array typed as `any[]` (from a JSON parse), the constraint degenerated, the result type was `any[]`. Downstream errors.

The vulnerable diff

tslib/pluck.ts
// lib/pluck.ts
function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
  return arr.map(o => o[key]);
}

// callers/show-users.ts
const data = JSON.parse(raw);             // type: any
const ids  = pluck(data, "id");           // K extends keyof any = string | number | symbol
                                          // ids: any[] — no useful type.

What is wrong

Generic constraints depend on the input type for their narrowing power. `<T, K extends keyof T>` works beautifully when T is a concrete type — `keyof T` then narrows K to the literal keys. When T is `any`, `keyof any` is `string | number | symbol`, which constrains nothing. The fix is either to fix the input type upstream (parse JSON into a known type with Zod or similar) or to require a narrower constraint via `as const` arrays in tests.

The attack

Symptom: linter green, runtime wrong:

textexploit
// pluck(data, "nonexistent")  // TS doesn't error if data: any
//                                      returns array of undefined, no warning.

Cascades: downstream code that expected `string[]` from `pluck(users, 'name')` gets `any[]`, misses other type errors.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 30 sec ago

[mesrai] [code-review] [Language] [Generics] [low]

`pluck` constraint depends on a typed input. When the input is
`any[]`, the constraint provides no inference and the result is
`any[]`.

Two fixes:

  1. Force a typed input at the call site by parsing JSON:

     const data = UserSchema.array().parse(JSON.parse(raw));
     // data: User[]
     const ids = pluck(data, "id");  // ids: User['id'][]

  2. Or change pluck's signature to refuse `any`:

     function pluck<T extends Record<string, unknown>, K extends keyof T>(
       arr: T[], key: K
     ): T[K][] { return arr.map(o => o[key]); }
     // Now pluck(anyArr, ...) won't compile.

Usually option 1 is right — the bug is at the JSON.parse boundary,
not the helper.

The fix

tscallers/show-users.ts (fixed)
// callers/show-users.ts — fixed
const UserSchema = z.object({ id: z.string(), name: z.string() });
const data = UserSchema.array().parse(JSON.parse(raw));
const ids  = pluck(data, "id");   // ids: string[]

Validate at the JSON boundary. The pluck helper now operates on a precisely typed input. Result type is exact.

Why human review missed it

Generic constraints behave correctly only with precise inputs. `any` poisons inference and there is no compile error to indicate it. Mesrai's rule pack catches generic calls where the type argument is inferred as `any`/`unknown` from a JSON parse or similar trust boundary.

Related rules + further reading

Mesrai rule pack: language/generic-any-poisoning — flags generic functions whose inference depends on a widened input type.

TypeScript handbook: Type Inference.

Common when integrating typed code with untyped JSON ingest.

Takeaway

Generics infer from inputs. Widen the input and inference degrades. Parse at boundaries; generics stay sharp.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.