Lesson 17 · Module 2.5 — Generics
<TArgs extends any[]> captures a whole signatureHow does a wrapper like safeFunction(fn) preserve any function’s
parameter list? The trick comes down to one constrained generic:
<TArgs extends any[]>(...args: TArgs). That any[] bound makes
TArgs stand for the whole parameter tuple — and that’s the difference
between a wrapper that works and one that fails.
<TArgs extends any[]> says “TArgs is a tuple type.” Combined
with ...args: TArgs, the entire argument list — [], [string],
[string, number] — becomes one captured thing TS can infer and re-spread.
TArgs extends any[]type PromiseFunc<TArgs extends any[], TResult> =
(...args: TArgs) => Promise<TResult>;
const safeFunction =
<TArgs extends any[], TResult>(func: PromiseFunc<TArgs, TResult>) =>
async (...args: TArgs) => { /* ...call func(...args) safely... */ };
Because TArgs is bound to any[], TS treats it as a tuple and infers
it straight from whatever function you hand in. A table of cases:
TArgs = [] // for () => Promise<T>
TArgs = [string] // for (s: string) => Promise<T>
TArgs = [string, number] // for (s: string, n: number) => Promise<T>
So ...args: TArgs on the returned wrapper re-emits the original signature exactly. Pass
async () => 123 and TS infers TArgs = [], giving a wrapper of type
() => Promise<number> — a perfect match.
First, why is a tuple even allowed? Because a constraint is a ceiling, not the
answer: extends any[] says “TArgs may be any array-like type,” and a tuple
is array-like ([string, number] is a subtype of any[]). So the bound
permits a tuple — it does not force the wide any[].
But “allowed” is not “chosen.” What actually produces the tuple is the rest
position. ...args: TArgs captures the function’s whole parameter list, and TypeScript
models a parameter list as a tuple — fixed length, a type per slot, even the labels. Matching
(s: string, n: number) against (...args: TArgs) reads off
TArgs = [s: string, n: number] exactly. (This rest-into-tuple inference was added in TS 3.0 for
exactly this “capture and replay a signature” case.)
An array-typed constraint does not narrow on its own. In Lesson 2.2,
<T extends string[]>(statuses: T) fed a plain array literal widened to
string[] — no tuple, even though string[] is just as much a “container” bound as
any[]. So the bound is not the deciding factor; the rest position capturing a
parameter list is — versus a direct parameter capturing an array value, which widens.
Same container constraint, opposite result. (The other way to turn a plain array value into a tuple is to
pin it with as const — also Lesson 2.2.)
TArgs unconstrainedDrop the extends any[] and change the spread to ...args: TArgs[], and the
meaning of TArgs silently shifts from “the tuple” to “one element of the tuple”:
type PromiseFunc<TArgs, TResult> =
(...args: TArgs[]) => Promise<TResult>; // TArgs is now an ELEMENT type
const safeFunction =
<TArgs, TResult>(func: PromiseFunc<TArgs, TResult>) =>
async (...args: TArgs[]) => { /* ... */ };
const func = safeFunction(async () => 123);
// ❌ () doesn't match (...args: TArgs[]); TS can't figure out what TArgs should be.
The diagnosis: with ...args: TArgs[], TArgs is the element type, so
TS must reverse-engineer “what element makes an array that matches ()?” — and there’s no
answer for a zero-argument function. Inference fails. You’d have to specify it by hand,
safeFunction<never, number>(...), which defeats the point.
| version | what TArgs means | infers from () => …? |
|---|---|---|
| <TArgs extends any[]> ...args: TArgs | the whole tuple | yes — TArgs = [] |
| <TArgs> ...args: TArgs[] | one element | no — unresolvable |
...args: TArgs and ...args: TArgs[] look almost identical but are opposite
ideas. TArgs (with the any[] bound) is the list;
TArgs[] is an array of one element type. The bound is the tell — read it first.
This is the engine behind every “wrap a function but keep its signature” helper — debouncers,
loggers, error-catchers. The whole capability rests on that one bound, extends any[],
turning TArgs into a tuple TS can both infer and spread back out.
Recall, don’t re-read. (Answers reveal on click.)
<TArgs extends any[]>(...args: TArgs), what does TArgs capture?...args: TArgs[] version fail on async () => 123?(s: string) => Promise<T>, the working version infers TArgs as…The official page is
TypeScript Handbook — Generics;
the tuple/rest mechanics are in
Tuple Types
and the release note on
tuples in rest parameters.
Best way to feel it: paste both safeFunction versions into the
TS Playground and hover the inferred TArgs.
Got a function wrapper that loses its signature? Paste it into the Playground and ask “is
TArgs the tuple or an element?” That single question, and the
extends any[] bound, is usually the whole fix.