mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
Pass TypeScript option literals to open JavaScript option parameters
`new Agent({ keepAliveTimeout: 10 })` against undici's
`constructor({ factory = defaultFactory, maxOrigins = Infinity, ...options }
= {})` failed with SC2002: the literal was laid out against the record the
checker infers for the pattern, which lacks the properties the rest
element collects.
When a TypeScript object literal flows into a checked-value slot whose
contextual type is a JavaScript destructuring pattern with a rest element
(or that pattern's `= {}` default), and the literal names a property the
type does not declare, the literal is built as the plain object the
JavaScript side receives. Every other literal keeps its previous lowering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7b1556e0f6
commit
3c76868fca
@@ -2112,6 +2112,51 @@ export class Lowerer {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
/** A TypeScript object literal handed to a JavaScript checked-value
|
||||
* parameter whose inferred type is open (`constructor({ factory, ...options
|
||||
* } = {})` — the rest element admits any property): the literal names a
|
||||
* property the contextual JS type does not declare, so it has no record
|
||||
* layout there and is built as the plain object the JS side receives. */
|
||||
private excessJsContextualProperty(
|
||||
literal: ts.ObjectLiteralExpression,
|
||||
contextual: readonly ts.Type[],
|
||||
): boolean {
|
||||
const objects = contextual.filter(
|
||||
(part) => !(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)),
|
||||
);
|
||||
if (objects.length === 0) return false;
|
||||
// Only a destructuring pattern with a rest element is open; other
|
||||
// inferred JS types keep their record lowering.
|
||||
const fromJs = objects.every((part) => {
|
||||
const sym = part.getSymbol();
|
||||
return (
|
||||
sym !== undefined &&
|
||||
this.checker.declarationsOf(sym).some((d) => {
|
||||
if (!isJsSourceFile(d.getSourceFile())) return false;
|
||||
// The pattern itself, or the `= {}` default of a pattern parameter.
|
||||
const pattern =
|
||||
ts.isObjectLiteralExpression(d) &&
|
||||
d.parent !== undefined &&
|
||||
ts.isParameter(d.parent) &&
|
||||
d.parent.initializer === d
|
||||
? d.parent.name
|
||||
: d;
|
||||
return (
|
||||
ts.isObjectBindingPattern(pattern) &&
|
||||
pattern.elements.nodes.some((element) => element.dotDotDotToken !== undefined)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
if (!fromJs) return false;
|
||||
return literal.properties.nodes.some((prop) => {
|
||||
if (!prop.name || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)))
|
||||
return false;
|
||||
const name = prop.name.text;
|
||||
return objects.every((part) => !this.checker.getPropertyOfType(part, name));
|
||||
});
|
||||
}
|
||||
|
||||
/** The index value of a keyed read of a record's index signature that
|
||||
* can answer undefined for a missing key while its checker type does not
|
||||
* admit undefined, or null. A literal key naming a declared field is a
|
||||
@@ -9615,7 +9660,8 @@ export class Lowerer {
|
||||
builtinOption("SpawnOptions") ||
|
||||
builtinOption("UrlObject") ||
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x))
|
||||
!this.mapTypeOf(this.typeOf(x)) ||
|
||||
this.excessJsContextualProperty(x, parts)
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// TypeScript object literals passed to a JavaScript constructor whose
|
||||
// options parameter is a destructuring pattern with a rest element: the
|
||||
// literal's extra properties reach the rest object.
|
||||
import { Agent } from "optkit";
|
||||
|
||||
console.log(new Agent().describe("a"));
|
||||
console.log(new Agent({ keepAliveTimeout: 10 }).describe("b"));
|
||||
console.log(new Agent({ maxOrigins: 2, keepAliveTimeout: 5, pipelining: 1 }).describe("c"));
|
||||
console.log(new Agent({ factory: (origin) => "client(" + origin + ")" }).describe("d"));
|
||||
|
||||
class TracingAgent extends Agent {}
|
||||
console.log(new TracingAgent({ pipelining: 3 }).describe("e"));
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export interface Options {
|
||||
factory?: (origin: string) => string;
|
||||
maxOrigins?: number;
|
||||
keepAliveTimeout?: number;
|
||||
pipelining?: number;
|
||||
}
|
||||
|
||||
export declare class Agent {
|
||||
constructor(opts?: Options);
|
||||
describe(origin: string): string;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
function defaultFactory(origin) {
|
||||
return 'pool(' + origin + ')';
|
||||
}
|
||||
|
||||
// undici's Agent constructor shape: named options with defaults, and a rest
|
||||
// element collecting every other option for the per-origin dispatchers.
|
||||
class Agent {
|
||||
constructor({ factory = defaultFactory, maxOrigins = Infinity, ...options } = {}) {
|
||||
this.factory = factory;
|
||||
this.maxOrigins = maxOrigins;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
describe(origin) {
|
||||
return this.factory(origin) + ' max=' + this.maxOrigins + ' ' + JSON.stringify(this.options);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Agent };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "optkit",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
Reference in New Issue
Block a user