mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
An interface's generic method whose own type parameter is constrained by the enclosing type's parameter (run<T extends TApi>(item: Item<T>)), implemented by a plain contextually typed lambda, compiles since #2274: the lambda takes the contextual signature's type parameters and lowers once, erased. The merged corpus only covers unconstrained contextual generics, so this program pins the constrained shape with a conditional member on the argument type, explicit and inferred call type arguments, the class-body form, and record-held receivers. Verified against the 2026-10-23 base checkout, where the same shape refused with SC2009 (Item<T>'s member 'api' has type 'T'), and the pi models.ts:1159 Model<T> 'compat' shape refused the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
// A generic method member whose own type parameter is CONSTRAINED by the
|
|
// enclosing type's type parameter (`run<T extends TApi>(item: Item<T>)`),
|
|
// implemented by a plain lambda: the lambda takes the interface signature's
|
|
// type parameters as its own and lowers once, erased like a generic lambda.
|
|
// `Item<T>`'s conditional member depends on the type argument each call
|
|
// passes, so the erased parameter must hold whatever the call sends.
|
|
|
|
type Api = "alpha" | "beta" | "gamma";
|
|
|
|
interface AlphaOptions {
|
|
fast?: boolean;
|
|
}
|
|
interface BetaOptions {
|
|
depth?: number;
|
|
}
|
|
|
|
interface Item<TApi extends Api> {
|
|
id: string;
|
|
api: TApi;
|
|
options?: TApi extends "alpha" ? AlphaOptions : TApi extends "beta" ? BetaOptions : never;
|
|
}
|
|
|
|
interface Runner<TApi extends Api> {
|
|
name: string;
|
|
run<T extends TApi>(item: Item<T>): string;
|
|
}
|
|
|
|
function describe(item: Item<Api>): string {
|
|
const options = item.options === undefined ? "-" : JSON.stringify(item.options);
|
|
return `${item.id}:${item.api}:${options}`;
|
|
}
|
|
|
|
function makeRunner<TApi extends Api>(name: string): Runner<TApi> {
|
|
return {
|
|
name,
|
|
run: (item) => `${name}:${describe(item)}`,
|
|
};
|
|
}
|
|
|
|
const both = makeRunner<"alpha" | "beta">("both");
|
|
console.log(both.name);
|
|
console.log(both.run({ id: "a", api: "alpha", options: { fast: true } }));
|
|
console.log(both.run({ id: "b", api: "beta", options: { depth: 3 } }));
|
|
console.log(both.run({ id: "c", api: "alpha" }));
|
|
console.log(both.run<"beta">({ id: "d", api: "beta" }));
|
|
|
|
const all = makeRunner<Api>("all");
|
|
console.log(all.run({ id: "e", api: "gamma" }));
|
|
console.log(all.run({ id: "f", api: "beta", options: { depth: 1 } }));
|
|
|
|
// The same contract through a class body: the generic method lowers under
|
|
// the class instantiation's binding joined with its own type parameter.
|
|
class ClassRunner<TApi extends Api> implements Runner<TApi> {
|
|
name: string;
|
|
constructor(name: string) {
|
|
this.name = name;
|
|
}
|
|
run<T extends TApi>(item: Item<T>): string {
|
|
return `${this.name}:${describe(item)}`;
|
|
}
|
|
}
|
|
|
|
const cls = new ClassRunner<"alpha" | "beta">("cls");
|
|
console.log(cls.run({ id: "g", api: "alpha", options: { fast: false } }));
|
|
console.log(cls.run({ id: "h", api: "beta" }));
|
|
|
|
// Record-held receivers call the erased slot the literal stored.
|
|
const runners: Runner<"alpha" | "beta">[] = [both, cls];
|
|
let combined = "";
|
|
for (const runner of runners) {
|
|
combined += `[${runner.run({ id: "i", api: "alpha" })}]`;
|
|
}
|
|
console.log(combined);
|