mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Narrow filtered union elements into a record with an arm's fields
A written type predicate whose target is no arm of the element union but the same fields as one, with a nested union field narrowed (Model<"a"> from the Model<Api> arm), was refused. The kept elements of that arm now convert field by field, the nested union by its tag; any other kept element still throws the predicate TypeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d1abf69149
commit
9eaa210e1f
@@ -7,6 +7,8 @@
|
||||
*
|
||||
* - a union element to one of its arms, or to a union of some of its
|
||||
* arms, by its tag;
|
||||
* - a union element to a record with the same fields as one of its arms,
|
||||
* through that arm;
|
||||
* - a checked-dynamic element by dynCheck;
|
||||
* - a class instance to a subclass by its class;
|
||||
* - a tuple or record to one with the same fields, field by field.
|
||||
@@ -229,10 +231,35 @@ function unionConversion(
|
||||
if (targetArms === null) return null;
|
||||
const body: IrStmt[] = [];
|
||||
let kept = 0;
|
||||
let recordArmConversions = 0;
|
||||
for (let tag = 0; tag < source.arms.length; tag++) {
|
||||
const arm = source.arms[tag]!;
|
||||
const targetTag = targetArms.findIndex((t) => typeEquals(t, arm));
|
||||
if (targetTag < 0) continue;
|
||||
if (targetTag < 0) {
|
||||
// A record target that is no arm itself but the same fields as one
|
||||
// (`Model<"a">` from the `Model<Api>` arm, whose nested union field
|
||||
// the predicate narrows): that arm converts field by field.
|
||||
if (to.kind !== "record" || arm.kind !== "record") continue;
|
||||
const narrowedArm: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag,
|
||||
value: input,
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
const converted = conversionBody(lowerer, narrowedArm, arm, to, typeText, loc);
|
||||
if (converted === null) continue;
|
||||
recordArmConversions++;
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc },
|
||||
then: converted,
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
kept++;
|
||||
const payload: IrExpr = isUnitType(arm)
|
||||
? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }
|
||||
@@ -249,8 +276,11 @@ function unionConversion(
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// Every arm the target names must be one of the union's own arms.
|
||||
if (kept === 0 || kept !== targetArms.length) return null;
|
||||
// Every arm the target names must be one of the union's own arms, or a
|
||||
// record target converts from at least one arm.
|
||||
if (recordArmConversions > 0) {
|
||||
if (kept !== 0) return null;
|
||||
} else if (kept === 0 || kept !== targetArms.length) return null;
|
||||
body.push(
|
||||
typeError(
|
||||
`a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/filter-predicate-model-narrow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// `.filter` with a written type predicate whose target is no arm of the
|
||||
// element union but the same fields as one, with a nested union field the
|
||||
// predicate narrows (`Model<"a">` from the `Model<Api>` arm). Kept elements
|
||||
// convert field by field; a kept element of another shape would throw.
|
||||
|
||||
interface CompatA {
|
||||
strict?: boolean;
|
||||
}
|
||||
interface CompatB {
|
||||
effort?: string;
|
||||
}
|
||||
interface CompatMap {
|
||||
a: CompatA;
|
||||
b: CompatB;
|
||||
}
|
||||
type Api = keyof CompatMap;
|
||||
interface Model<A extends Api> {
|
||||
kind: "chat";
|
||||
api: A;
|
||||
id: string;
|
||||
window: number;
|
||||
compat?: CompatMap[A];
|
||||
}
|
||||
interface Image {
|
||||
kind: "image";
|
||||
id: string;
|
||||
}
|
||||
type AnyModel = Model<Api> | Image;
|
||||
|
||||
function restore(stored: AnyModel[]): Model<"a">[] {
|
||||
return stored.filter((model): model is Model<"a"> => model.kind === "chat" && model.api === "a");
|
||||
}
|
||||
|
||||
const stored: AnyModel[] = [
|
||||
{ kind: "chat", api: "a", id: "1", window: 8, compat: { strict: true } },
|
||||
{ kind: "image", id: "2" },
|
||||
{ kind: "chat", api: "b", id: "3", window: 4, compat: { effort: "hi" } },
|
||||
{ kind: "chat", api: "a", id: "4", window: 2 },
|
||||
];
|
||||
const restored = restore(stored);
|
||||
for (const model of restored) console.log(model.id, model.api, model.window, model.compat?.strict, "compat" in model);
|
||||
console.log(restored.length, restored.map((model) => model.window).reduce((a, b) => a + b, 0));
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4307, "san": 81017 }
|
||||
Reference in New Issue
Block a user