mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Narrow filter results through written type predicates with checked conversions
`.filter` with a written type predicate (`(p): p is Cat => ...`, or a guard function passed as a value such as `filter(isDog)`) was refused with SC1090, because TypeScript does not check the predicate. An inferred predicate that keeps several union arms (`v => v !== undefined` over `(string | number | undefined)[]`) was refused too. The filter loop now keeps the receiver's element type, and the result converts to the predicate's type element by element (filter-predicates.ts): - a union element to one of its arms, or to a union of some of its arms, by its tag; - 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. An element the predicate's type does not describe, kept because the predicate returned true for it, throws a catchable TypeError: "a kept element is not a 'Cat' (a type predicate returned true for a value of another type)". The diagnostics fixture filter-narrow.ts, which pinned the three refusals, compiles now; corpus 7007 covers its cases. Fixes #1680.
This commit is contained in:
@@ -59,6 +59,7 @@ import {
|
||||
} from "./array-iteration.js";
|
||||
import { dynamicReceiverThrows } from "./dynamic-receivers.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
|
||||
import { checkedFilterNarrowing } from "./filter-predicates.js";
|
||||
|
||||
/** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct
|
||||
* call of a synthetic module function — one per method + element/result
|
||||
@@ -123,10 +124,11 @@ export function lowerArrayHofCall(
|
||||
}
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
}
|
||||
const outElem =
|
||||
const filtered =
|
||||
method === "filter"
|
||||
? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter)
|
||||
: callbackArrayElement(lowerer, call, fnRet);
|
||||
: null;
|
||||
const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet);
|
||||
// A filter that re-tags its elements keeps the single union-typed visit.
|
||||
const fast =
|
||||
present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem))
|
||||
@@ -145,19 +147,33 @@ export function lowerArrayHofCall(
|
||||
present,
|
||||
);
|
||||
const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID;
|
||||
return {
|
||||
const result: IrExpr = {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [receiver, fnArg, ...(fast ? [fast] : [])],
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
const narrowing = filtered?.narrowing ?? null;
|
||||
if (narrowing === null) return result;
|
||||
return {
|
||||
kind: "call",
|
||||
callee: narrowing.helper,
|
||||
args: [result],
|
||||
type: arrayOf(narrowing.elem),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The checker-selected element of a filter result, with the same trust
|
||||
* boundary as the historical narrowing filter: an inferred inline type
|
||||
* predicate may prove one source arm; a written predicate annotation may
|
||||
* not. Boolean is safe when every discarded arm is a nullish unit. */
|
||||
/** The element a filter keeps, and the checked conversion of the result
|
||||
* to the checker's narrower element when the predicate does not prove it.
|
||||
* An inferred inline type predicate proves one source arm, or a class
|
||||
* view, so the loop keeps that type directly. A written predicate (`(p): p
|
||||
* is Cat => ...`, a guard function passed as a value) is an assertion
|
||||
* TypeScript does not check, and an inferred predicate that keeps several
|
||||
* arms has no single arm to re-tag: the loop keeps the receiver's element
|
||||
* and `narrowing` (checkedFilterNarrowing) converts each kept element.
|
||||
* Boolean is safe when every discarded arm is a nullish unit. */
|
||||
function filterResultElem(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -165,7 +181,37 @@ function filterResultElem(
|
||||
elem: IrType,
|
||||
valueT: IrType,
|
||||
booleanFilter: boolean,
|
||||
): IrType {
|
||||
): { elem: IrType; narrowing: { helper: string; elem: IrType } | null } {
|
||||
const kept = filterKeptElem(lowerer, call, argNode, elem, valueT, booleanFilter);
|
||||
if (typeof kept !== "string") return { elem: kept, narrowing: null };
|
||||
return { elem, narrowing: { helper: kept, elem: filterCheckerElem(lowerer, call, elem) } };
|
||||
}
|
||||
|
||||
/** The checker's spelling of a filter result's element type. */
|
||||
function filterTypeText(lowerer: Lowerer, call: ts.CallExpression): string {
|
||||
const result = lowerer.typeOf(call);
|
||||
const element = lowerer.checker.isArrayType(result)
|
||||
? lowerer.checker.getTypeArguments(result as ts.TypeReference)[0]
|
||||
: undefined;
|
||||
return element === undefined ? "" : lowerer.checker.typeToString(element);
|
||||
}
|
||||
|
||||
/** The checker's element of a filter result, or the receiver's when the
|
||||
* filter introduces no refinement. */
|
||||
function filterCheckerElem(lowerer: Lowerer, call: ts.CallExpression, elem: IrType): IrType {
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
return result?.kind === "array" ? result.elem : elem;
|
||||
}
|
||||
|
||||
/** filterResultElem's element, or the name of the checked narrowing helper. */
|
||||
function filterKeptElem(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
argNode: ts.Expression,
|
||||
elem: IrType,
|
||||
valueT: IrType,
|
||||
booleanFilter: boolean,
|
||||
): IrType | string {
|
||||
const callT = lowerer.typeOf(call);
|
||||
const result = lowerer.mapTypeOf(callT);
|
||||
let outElem = result?.kind === "array" ? result.elem : elem;
|
||||
@@ -191,23 +237,31 @@ function filterResultElem(
|
||||
"keep the receiver's element type instead — annotate the callback's return ': boolean' " +
|
||||
"(the checker then skips the predicate) or annotate the result with the receiver's own " +
|
||||
"element type — and narrow the elements after";
|
||||
if (!booleanFilter) {
|
||||
const written =
|
||||
!booleanFilter &&
|
||||
((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) ||
|
||||
argNode.type !== undefined);
|
||||
const narrowing = booleanFilter
|
||||
? null
|
||||
: (): string | null =>
|
||||
checkedFilterNarrowing(lowerer, elem, outElem, filterTypeText(lowerer, call), locOf(call));
|
||||
if (written) {
|
||||
const helper = narrowing!();
|
||||
if (helper !== null) return helper;
|
||||
if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` +
|
||||
`the checker inferred can re-tag — ${annotateEscape})`,
|
||||
);
|
||||
}
|
||||
if (argNode.type) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` +
|
||||
`unchecked assertion nothing validates at runtime — ${annotateEscape})`,
|
||||
`narrowing '.filter' to '${lowerer.fmt(outElem)}' through a callback VALUE (no checked ` +
|
||||
`conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`,
|
||||
);
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.filter' to '${lowerer.fmt(outElem)}' with a hand-written type predicate (no ` +
|
||||
`checked conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`,
|
||||
);
|
||||
}
|
||||
const classRefinement =
|
||||
!booleanFilter && outElem.kind === "object" && canAssertClassValue(lowerer, valueT, outElem);
|
||||
@@ -215,6 +269,8 @@ function filterResultElem(
|
||||
!classRefinement &&
|
||||
(valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0)
|
||||
) {
|
||||
const helper = narrowing?.() ?? null;
|
||||
if (helper !== null) return helper;
|
||||
const multiArm = outElem.kind === "union";
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2650,6 +2706,7 @@ export function lowerTuplePredicateCall(
|
||||
// the value type an array find narrows from.
|
||||
let foundT: IrType = elem;
|
||||
let retag: string | null = null;
|
||||
let narrowing: { helper: string; elem: IrType } | null = null;
|
||||
if (method === "filter") {
|
||||
// The element union is built in position order, so the checker's
|
||||
// result element can list the same arms in another order.
|
||||
@@ -2660,9 +2717,11 @@ export function lowerTuplePredicateCall(
|
||||
checkerResult?.kind === "array" &&
|
||||
arms(checkerResult.elem).length === arms(elem).length &&
|
||||
arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other)));
|
||||
outElem = unchanged
|
||||
? elem
|
||||
: filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter);
|
||||
if (!unchanged) {
|
||||
const filtered = filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter);
|
||||
outElem = filtered.elem;
|
||||
narrowing = filtered.narrowing;
|
||||
}
|
||||
resultT = arrayOf(outElem);
|
||||
} else if (method === "find") {
|
||||
resultT = lowerer.irTypeOf(call);
|
||||
@@ -2795,5 +2854,19 @@ export function lowerTuplePredicateCall(
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
|
||||
const result: IrExpr = {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [receiver, fnArg],
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
if (narrowing === null) return result;
|
||||
return {
|
||||
kind: "call",
|
||||
callee: narrowing.helper,
|
||||
args: [result],
|
||||
type: arrayOf(narrowing.elem),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/* Checked narrowing for `.filter` with a written type predicate.
|
||||
*
|
||||
* A written predicate (`(p): p is Cat => ...`, or a guard function passed
|
||||
* as a value) is an assertion TypeScript does not check, so the filter loop
|
||||
* keeps the receiver's element type and the result converts to the
|
||||
* predicate's type afterwards, element by element:
|
||||
*
|
||||
* - a union element to one of its arms, or to a union of some of its
|
||||
* arms, by its tag;
|
||||
* - 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.
|
||||
*
|
||||
* Each conversion throws the catchable TypeError of a checked extraction
|
||||
* when a kept element is not of the predicate's type, which is when the
|
||||
* predicate returned true for a value it does not describe. */
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
STRING,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import { checkedClassAssertion } from "../class-assertions.js";
|
||||
import { buildArrayConversion } from "../coercions/builders.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
|
||||
/** The interned helper converting a `from[]` filter result to `to[]`, or
|
||||
* null when an element of `from` has no checked conversion to `to`.
|
||||
* `typeText` names the predicate's type in the TypeError. */
|
||||
export function checkedFilterNarrowing(
|
||||
lowerer: Lowerer,
|
||||
from: IrType,
|
||||
to: IrType,
|
||||
typeText: string,
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
const element = elementHelper(lowerer, from, to, loc, typeText);
|
||||
if (element === null) return null;
|
||||
const key = `filter.narrow:${typeKey(from)}:${typeKey(to)}:${typeText}`;
|
||||
const existing = lowerer.valueHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%arr.narrow", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const outT: IrType = { kind: "array", elem: to };
|
||||
lowerer.liftedFns.push(
|
||||
buildArrayConversion(
|
||||
name,
|
||||
{ kind: "array", elem: from },
|
||||
{ kind: "arrayLit", elems: [], type: outT, loc },
|
||||
(value, _index, result) => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: result,
|
||||
args: [{ kind: "call", callee: element, args: [value], type: to, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
),
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The interned helper converting one `from` value to `to`, or null. */
|
||||
function elementHelper(
|
||||
lowerer: Lowerer,
|
||||
from: IrType,
|
||||
to: IrType,
|
||||
loc: SrcLoc,
|
||||
typeText = lowerer.fmt(to),
|
||||
): string | null {
|
||||
const key = `filter.narrow.value:${typeKey(from)}:${typeKey(to)}:${typeText}`;
|
||||
const existing = lowerer.valueHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
// A recursive type pair converts through the helper being built.
|
||||
if (lowerer.coercions.planning.has(key)) return null;
|
||||
lowerer.coercions.planning.add(key);
|
||||
const input = varRef("v.0", from, loc);
|
||||
let body: IrStmt[] | null;
|
||||
try {
|
||||
body = conversionBody(lowerer, input, from, to, typeText, loc);
|
||||
} finally {
|
||||
lowerer.coercions.planning.delete(key);
|
||||
}
|
||||
if (body === null) return null;
|
||||
const name = keyedHelperName(lowerer, "%narrow.checked", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "v.0", name: "v", type: from }],
|
||||
returnType: to,
|
||||
locals: [{ id: "v.0", name: "v", type: from, mutable: false }],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** A converted value: `value` itself when the types are equal, or a call of
|
||||
* the conversion helper. */
|
||||
function converted(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
from: IrType,
|
||||
to: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (typeEquals(from, to)) return value;
|
||||
const helper = elementHelper(lowerer, from, to, loc);
|
||||
return helper === null ? null : { kind: "call", callee: helper, args: [value], type: to, loc };
|
||||
}
|
||||
|
||||
/** The TypeError a kept element of another type throws. */
|
||||
function typeError(message: string, loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "throw",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "error.new",
|
||||
args: [{ kind: "strLit", value: message, type: STRING, loc }],
|
||||
type: { kind: "object", className: "%TypeError" },
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function conversionBody(
|
||||
lowerer: Lowerer,
|
||||
input: IrExpr,
|
||||
from: IrType,
|
||||
to: IrType,
|
||||
typeText: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] | null {
|
||||
if (typeEquals(from, to)) return [{ kind: "return", value: input, loc }];
|
||||
if (from.kind === "dyn")
|
||||
return [{ kind: "return", value: { kind: "dynCheck", value: input, type: to, loc }, loc }];
|
||||
if (from.kind === "object" && to.kind === "object") {
|
||||
const asserted = checkedClassAssertion(lowerer, input, to, loc);
|
||||
return asserted === null ? null : [{ kind: "return", value: asserted, loc }];
|
||||
}
|
||||
if (from.kind === "union")
|
||||
return unionConversion(lowerer, input, from.unionId, to, typeText, loc);
|
||||
if (from.kind === "record" && to.kind === "record") {
|
||||
const source = lowerer.shapes.get(from.shapeId);
|
||||
const target = lowerer.shapes.get(to.shapeId);
|
||||
if (
|
||||
!source ||
|
||||
!target ||
|
||||
source.tuple !== target.tuple ||
|
||||
source.indexValue ||
|
||||
target.indexValue
|
||||
)
|
||||
return null;
|
||||
if (source.fields.length !== target.fields.length) return null;
|
||||
const fields: { name: string; value: IrExpr }[] = [];
|
||||
for (const field of target.fields) {
|
||||
const own = source.fields.find((f) => f.name === field.name);
|
||||
if (!own) return null;
|
||||
const read: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: input,
|
||||
shapeId: from.shapeId,
|
||||
field: field.name,
|
||||
type: own.type,
|
||||
loc,
|
||||
};
|
||||
const value = converted(lowerer, read, own.type, field.type, loc);
|
||||
if (value === null) return null;
|
||||
fields.push({ name: field.name, value });
|
||||
}
|
||||
return [{ kind: "return", value: { kind: "recordLit", fields, type: to, loc }, loc }];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A union value to one of its arms, or to a union of some of its arms:
|
||||
* each arm the target holds returns, any other arm throws. */
|
||||
function unionConversion(
|
||||
lowerer: Lowerer,
|
||||
input: IrExpr,
|
||||
unionId: string,
|
||||
to: IrType,
|
||||
typeText: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] | null {
|
||||
const source = lowerer.unions.get(unionId);
|
||||
if (!source) return null;
|
||||
const targetArms = to.kind === "union" ? (lowerer.unions.get(to.unionId)?.arms ?? null) : [to];
|
||||
if (targetArms === null) return null;
|
||||
const body: IrStmt[] = [];
|
||||
let kept = 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;
|
||||
kept++;
|
||||
const payload: IrExpr = isUnitType(arm)
|
||||
? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }
|
||||
: { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc };
|
||||
const value: IrExpr =
|
||||
to.kind === "union"
|
||||
? { kind: "unionWrap", unionId: to.unionId, tag: targetTag, value: payload, type: to, loc }
|
||||
: payload;
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc },
|
||||
then: [{ kind: "return", value, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// Every arm the target names must be one of the union's own arms.
|
||||
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)`,
|
||||
loc,
|
||||
),
|
||||
);
|
||||
return body;
|
||||
}
|
||||
@@ -6668,6 +6668,15 @@ export function lowerFilterNarrowCall(
|
||||
}
|
||||
const narrowed = outElem !== null && !typeEquals(outElem, elem);
|
||||
if (!narrowed && !isBooleanArg) return null;
|
||||
// A written predicate (`(p): p is Cat => ...`, a guard function value)
|
||||
// proves nothing at run time: the generic array HOF path filters the
|
||||
// receiver's elements and converts the kept ones checked
|
||||
// (filterResultElem).
|
||||
if (
|
||||
!isBooleanArg &&
|
||||
((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) || argNode.type)
|
||||
)
|
||||
return null;
|
||||
|
||||
const annotateEscape =
|
||||
"keep the receiver's element type instead — annotate the callback's return ': boolean' " +
|
||||
@@ -6679,6 +6688,9 @@ export function lowerFilterNarrowCall(
|
||||
lowerer.badType(call, callT); // defensive: a narrowed non-union receiver
|
||||
}
|
||||
tag = lowerer.armTag(elem.unionId, outElem);
|
||||
// Several kept arms: the generic path converts the kept elements to
|
||||
// the narrower union (filterResultElem).
|
||||
if (tag < 0 && !isBooleanArg) return null;
|
||||
if (tag < 0) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -6700,25 +6712,8 @@ export function lowerFilterNarrowCall(
|
||||
return { kind: "call", callee: helper, args: [receiver], type: arrayOf(outElem ?? elem), loc };
|
||||
}
|
||||
|
||||
// Inferred type predicate: inline function literal, NO return
|
||||
// annotation (a written one is an unchecked assertion), and the
|
||||
// checker reports a predicate over parameter 0.
|
||||
if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.filter' through a callback VALUE ` +
|
||||
`(only an inline callback whose predicate the checker inferred can re-tag — ${annotateEscape})`,
|
||||
);
|
||||
}
|
||||
if (argNode.type) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`narrowing '.filter' with a hand-written type predicate ` +
|
||||
`(a written 'x is T' is an unchecked assertion nothing validates at runtime — ${annotateEscape})`,
|
||||
);
|
||||
}
|
||||
// An inferred type predicate: an inline function literal with no return
|
||||
// annotation, over parameter 0 (written predicates returned above).
|
||||
// The receiver evaluates FIRST, in the enclosing function, like JS.
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const fnArg = lowerer.lowerExpr(argNode);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7007-filter-written-type-predicates.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/filter-narrow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// `.filter` with a written type predicate: an annotated callback
|
||||
// (`(p): p is Cat => ...`) or a guard function passed as a value. The
|
||||
// result holds the predicate's type: a union arm, a union of arms, a
|
||||
// number out of `unknown`, a record or tuple with a narrower field, or a
|
||||
// subclass. An inferred predicate that keeps several arms narrows too.
|
||||
interface Cat {
|
||||
kind: "cat";
|
||||
meow: string;
|
||||
}
|
||||
interface Dog {
|
||||
kind: "dog";
|
||||
bark: number;
|
||||
}
|
||||
interface Fish {
|
||||
kind: "fish";
|
||||
fins: number;
|
||||
}
|
||||
type Pet = Cat | Dog | Fish;
|
||||
const pets: Pet[] = [
|
||||
{ kind: "cat", meow: "m" },
|
||||
{ kind: "dog", bark: 2 },
|
||||
{ kind: "fish", fins: 3 },
|
||||
{ kind: "dog", bark: 5 },
|
||||
];
|
||||
|
||||
const cats = pets.filter((p): p is Cat => p.kind === "cat");
|
||||
console.log(cats.length, cats[0]!.meow);
|
||||
const furry = pets.filter((p): p is Cat | Dog => p.kind !== "fish");
|
||||
console.log(furry.map((p) => (p.kind === "cat" ? p.meow : p.bark)).join(","));
|
||||
const notFish = pets.filter((p) => p.kind !== "fish");
|
||||
console.log(notFish.length);
|
||||
|
||||
function isDog(p: Pet): p is Dog {
|
||||
return p.kind === "dog";
|
||||
}
|
||||
console.log(pets.filter(isDog).map((d) => d.bark * 10).join(","));
|
||||
|
||||
const isNumber = (v: unknown): v is number => typeof v === "number";
|
||||
const values: unknown[] = JSON.parse('[1, "a", 2, null, 4]');
|
||||
console.log(values.filter(isNumber).reduce((sum, n) => sum + n, 0));
|
||||
|
||||
const rows: unknown[] = JSON.parse('[{"id":1},{"id":"x"},{"id":3}]');
|
||||
const withIds = rows.filter(
|
||||
(r): r is { id: number } => typeof (r as { id: unknown }).id === "number",
|
||||
);
|
||||
console.log(withIds.map((r) => r.id + 1).join(","));
|
||||
|
||||
const entries: [string, unknown][] = [
|
||||
["a", 1],
|
||||
["b", "x"],
|
||||
["c", 3],
|
||||
];
|
||||
const numeric = entries.filter((e): e is [string, number] => typeof e[1] === "number");
|
||||
console.log(numeric.map(([key, value]) => `${key}${value * 2}`).join(" "));
|
||||
|
||||
class Shape {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
class Circle extends Shape {
|
||||
r: number;
|
||||
constructor(r: number) {
|
||||
super("circle");
|
||||
this.r = r;
|
||||
}
|
||||
}
|
||||
const shapes: Shape[] = [new Shape("square"), new Circle(2), new Circle(3)];
|
||||
const circles = shapes.filter((s): s is Circle => s instanceof Circle);
|
||||
console.log(circles.length, circles.map((c) => c.r).join("+"));
|
||||
|
||||
const mixed: (string | number | boolean)[] = ["a", 1, true, "b"];
|
||||
console.log(mixed.filter((v): v is string => typeof v === "string").join(""));
|
||||
console.log(mixed.filter((v): v is string | number => typeof v !== "boolean").length);
|
||||
|
||||
const pair: [string | number, string | number] = ["x", 4];
|
||||
console.log(pair.filter((v): v is number => typeof v === "number").map((n) => n + 1).join());
|
||||
|
||||
const optional: (string | number | undefined)[] = ["a", 1, undefined, 2];
|
||||
const defined = optional.filter((v) => v !== undefined);
|
||||
console.log(defined.length, defined.map((v) => typeof v).join(","));
|
||||
function isText(v: string | undefined): v is string {
|
||||
return typeof v === "string";
|
||||
}
|
||||
const maybe: (string | undefined)[] = ["a", undefined, "b"];
|
||||
console.log(maybe.filter(isText).join("-"));
|
||||
@@ -1,22 +0,0 @@
|
||||
// Narrowing filters re-tag ONLY what the runtime test proved: an inferred
|
||||
// single-arm predicate or filter(Boolean). Everything else stays fenced —
|
||||
// multi-arm targets (the re-tag doesn't exist), hand-written predicates
|
||||
// (unchecked assertions), and predicate VALUES (their annotation is
|
||||
// written by definition).
|
||||
const xs: (string | number | undefined)[] = ["a", 1, undefined];
|
||||
|
||||
// Multi-arm narrowing: string | number is not a single arm.
|
||||
const defined = xs.filter((x) => x !== undefined);
|
||||
console.log(defined.length);
|
||||
|
||||
// A hand-written predicate on an inline callback.
|
||||
const ys: (string | undefined)[] = ["a", undefined];
|
||||
const strings = ys.filter((x): x is string => typeof x === "string");
|
||||
console.log(strings.length);
|
||||
|
||||
// A predicate VALUE (declared function with a written predicate).
|
||||
function isString(x: string | undefined): x is string {
|
||||
return typeof x === "string";
|
||||
}
|
||||
const viaValue = ys.filter(isString);
|
||||
console.log(viaValue.length);
|
||||
@@ -1,20 +0,0 @@
|
||||
filter-narrow.ts:9:17 - error SC1090: '.filter' narrowing 'number | string | undefined' elements to the multi-arm 'number | string' (only a SINGLE arm re-tags — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet
|
||||
|
||||
8 | // Multi-arm narrowing: string | number is not a single arm.
|
||||
9 | const defined = xs.filter((x) => x !== undefined);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
10 | console.log(defined.length);
|
||||
|
||||
filter-narrow.ts:14:27 - error SC1090: narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an unchecked assertion nothing validates at runtime — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet
|
||||
|
||||
13 | const ys: (string | undefined)[] = ["a", undefined];
|
||||
14 | const strings = ys.filter((x): x is string => typeof x === "string");
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
15 | console.log(strings.length);
|
||||
|
||||
filter-narrow.ts:21:28 - error SC1090: narrowing '.filter' through a callback VALUE (only an inline callback whose predicate the checker inferred can re-tag — keep the receiver's element type instead — annotate the callback's return ': boolean' (the checker then skips the predicate) or annotate the result with the receiver's own element type — and narrow the elements after) is not supported yet
|
||||
|
||||
20 | }
|
||||
21 | const viaValue = ys.filter(isString);
|
||||
| ^~~~~~~~
|
||||
22 | console.log(viaValue.length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3510, "san": 2873 }
|
||||
Reference in New Issue
Block a user