Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pr 50377 redo filter boolean overload #56357

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/compiler/checker.ts
Expand Up @@ -34120,6 +34120,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
result = chooseOverload(candidates, assignableRelation, isSingleNonGenericCandidate, signatureHelpTrailingComma);
}
if (result) {
const returnType = calculateSignatureReturnTypeForSpecialCases(result, args);
if (returnType) {
result = cloneSignature(result);
result.resolvedReturnType = returnType;
}
return result;
}

Expand Down Expand Up @@ -34235,6 +34240,70 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

return result;

function calculateSignatureReturnTypeForSpecialCases(signature: Readonly<Signature>, args: readonly Expression[]): Type | undefined {
if (args.length >= 1) {
// In some tsx cases "symbol" is undefined, even though it is defined in the typechecker. So add ?
if (
signature.declaration?.symbol?.escapedName === "filter" && (
signature.declaration?.symbol?.parent?.escapedName === "Array"
|| signature.declaration?.symbol?.parent?.escapedName === "ReadonlyArray"
)
) {
const arg0Type = getTypeOfExpression(args[0]);
// This is safe even if a different BooleanConstructor is defined in a namespace,
// because in that case arg0Type.symbol.escapedName will appear as "__type".
if (arg0Type.symbol?.escapedName === "BooleanConstructor") {
// It is a-priori knowledge the filter returns the same type as the array type
// for a signature succeeding when BooleanConstructor is the argument type
let returnType = (signature.mapper as undefined | { targets: readonly Type[]; })?.targets[1];
// result.declaration?.symbol.parent?.escapedName==="ReadonlyArray"
if (returnType) {
const nonFalsieArrayTypesOut: Type[] = [];
// the return type can only be an array type.
// It cant actually be a union of array types for a single signature.
// So this forEachType could be skipped, but may be used in the future with union of array types
forEachType(returnType, at => {
let elemType: Type;
if (isTupleType(at)) {
// The tuple elements are unionized, *abondoning* the tupleness becuase
// filtering could create result of varying length.
// For variable length tuples, undefined is *not* added to the union within getElementTypes.
elemType = getUnionType(getElementTypes(at));
}
else if (isTupleLikeType(at)) {
// doesn't handle tupleLikeTypes
// just return the orginal type
nonFalsieArrayTypesOut.push(at);
return;
}
else {
elemType = getElementTypeOfArrayType(at) || anyType; // need test case for anyType
}
const nonFalsieElemTypes: Type[] = [];
nonFalsieElemTypes.push(filterType(
elemType,
t => {
const facts = getTypeFacts(t, TypeFacts.Truthy | TypeFacts.Falsy);
if (facts === TypeFacts.Falsy) {
return false;
}
else {
return true;
}
},
));
// output arrays are not not readonly
const atout = createArrayType(getUnionType(nonFalsieElemTypes));
nonFalsieArrayTypesOut.push(atout);
});
returnType = getUnionType(nonFalsieArrayTypesOut);
return returnType;
}
}
}
}
}

function addImplementationSuccessElaboration(failed: Signature, diagnostic: Diagnostic) {
const oldCandidatesForArgumentError = candidatesForArgumentError;
const oldCandidateForArgumentArityError = candidateForArgumentArityError;
Expand Down
32 changes: 32 additions & 0 deletions tests/baselines/reference/arrayFilterBooleanOverload.js
@@ -0,0 +1,32 @@
//// [tests/cases/compiler/arrayFilterBooleanOverload.ts] ////

//// [arrayFilterBooleanOverload.ts]
const nullableValues = ['a', 'b', null]; // expect (string | null)[]

const values1 = nullableValues.filter(Boolean); // expect string[]

// @ts-expect-error
const values2 = nullableValues.filter(new Boolean);

const arr = [0, 1, "", "foo", null] as const;

const arr2 = arr.filter(Boolean); // expect ("foo" | 1)[]



//// [arrayFilterBooleanOverload.js]
"use strict";
const nullableValues = ['a', 'b', null]; // expect (string | null)[]
const values1 = nullableValues.filter(Boolean); // expect string[]
// @ts-expect-error
const values2 = nullableValues.filter(new Boolean);
const arr = [0, 1, "", "foo", null];
const arr2 = arr.filter(Boolean); // expect ("foo" | 1)[]


//// [arrayFilterBooleanOverload.d.ts]
declare const nullableValues: (string | null)[];
declare const values1: string[];
declare const values2: (string | null)[];
declare const arr: readonly [0, 1, "", "foo", null];
declare const arr2: (1 | "foo")[];
33 changes: 33 additions & 0 deletions tests/baselines/reference/arrayFilterBooleanOverload.symbols
@@ -0,0 +1,33 @@
//// [tests/cases/compiler/arrayFilterBooleanOverload.ts] ////

=== arrayFilterBooleanOverload.ts ===
const nullableValues = ['a', 'b', null]; // expect (string | null)[]
>nullableValues : Symbol(nullableValues, Decl(arrayFilterBooleanOverload.ts, 0, 5))

const values1 = nullableValues.filter(Boolean); // expect string[]
>values1 : Symbol(values1, Decl(arrayFilterBooleanOverload.ts, 2, 5))
>nullableValues.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>nullableValues : Symbol(nullableValues, Decl(arrayFilterBooleanOverload.ts, 0, 5))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Boolean : Symbol(Boolean, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

// @ts-expect-error
const values2 = nullableValues.filter(new Boolean);
>values2 : Symbol(values2, Decl(arrayFilterBooleanOverload.ts, 5, 5))
>nullableValues.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>nullableValues : Symbol(nullableValues, Decl(arrayFilterBooleanOverload.ts, 0, 5))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Boolean : Symbol(Boolean, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

const arr = [0, 1, "", "foo", null] as const;
>arr : Symbol(arr, Decl(arrayFilterBooleanOverload.ts, 7, 5))
>const : Symbol(const)

const arr2 = arr.filter(Boolean); // expect ("foo" | 1)[]
>arr2 : Symbol(arr2, Decl(arrayFilterBooleanOverload.ts, 9, 5))
>arr.filter : Symbol(ReadonlyArray.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>arr : Symbol(arr, Decl(arrayFilterBooleanOverload.ts, 7, 5))
>filter : Symbol(ReadonlyArray.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Boolean : Symbol(Boolean, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))


45 changes: 45 additions & 0 deletions tests/baselines/reference/arrayFilterBooleanOverload.types
@@ -0,0 +1,45 @@
//// [tests/cases/compiler/arrayFilterBooleanOverload.ts] ////

=== arrayFilterBooleanOverload.ts ===
const nullableValues = ['a', 'b', null]; // expect (string | null)[]
>nullableValues : (string | null)[]
>['a', 'b', null] : (string | null)[]
>'a' : "a"
>'b' : "b"

const values1 = nullableValues.filter(Boolean); // expect string[]
>values1 : string[]
>nullableValues.filter(Boolean) : string[]
>nullableValues.filter : { <S extends string | null>(predicate: (value: string | null, index: number, array: (string | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | null, index: number, array: (string | null)[]) => unknown, thisArg?: any): (string | null)[]; }
>nullableValues : (string | null)[]
>filter : { <S extends string | null>(predicate: (value: string | null, index: number, array: (string | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | null, index: number, array: (string | null)[]) => unknown, thisArg?: any): (string | null)[]; }
>Boolean : BooleanConstructor

// @ts-expect-error
const values2 = nullableValues.filter(new Boolean);
>values2 : (string | null)[]
>nullableValues.filter(new Boolean) : (string | null)[]
>nullableValues.filter : { <S extends string | null>(predicate: (value: string | null, index: number, array: (string | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | null, index: number, array: (string | null)[]) => unknown, thisArg?: any): (string | null)[]; }
>nullableValues : (string | null)[]
>filter : { <S extends string | null>(predicate: (value: string | null, index: number, array: (string | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: string | null, index: number, array: (string | null)[]) => unknown, thisArg?: any): (string | null)[]; }
>new Boolean : Boolean
>Boolean : BooleanConstructor

const arr = [0, 1, "", "foo", null] as const;
>arr : readonly [0, 1, "", "foo", null]
>[0, 1, "", "foo", null] as const : readonly [0, 1, "", "foo", null]
>[0, 1, "", "foo", null] : readonly [0, 1, "", "foo", null]
>0 : 0
>1 : 1
>"" : ""
>"foo" : "foo"

const arr2 = arr.filter(Boolean); // expect ("foo" | 1)[]
>arr2 : (1 | "foo")[]
>arr.filter(Boolean) : (1 | "foo")[]
>arr.filter : { <S extends "" | 0 | 1 | "foo" | null>(predicate: (value: "" | 0 | 1 | "foo" | null, index: number, array: readonly ("" | 0 | 1 | "foo" | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: "" | 0 | 1 | "foo" | null, index: number, array: readonly ("" | 0 | 1 | "foo" | null)[]) => unknown, thisArg?: any): ("" | 0 | 1 | "foo" | null)[]; }
>arr : readonly [0, 1, "", "foo", null]
>filter : { <S extends "" | 0 | 1 | "foo" | null>(predicate: (value: "" | 0 | 1 | "foo" | null, index: number, array: readonly ("" | 0 | 1 | "foo" | null)[]) => value is S, thisArg?: any): S[]; (predicate: (value: "" | 0 | 1 | "foo" | null, index: number, array: readonly ("" | 0 | 1 | "foo" | null)[]) => unknown, thisArg?: any): ("" | 0 | 1 | "foo" | null)[]; }
>Boolean : BooleanConstructor


38 changes: 38 additions & 0 deletions tests/baselines/reference/unionOfArraysBooleanFilterCall.js
@@ -0,0 +1,38 @@
//// [tests/cases/compiler/unionOfArraysBooleanFilterCall.ts] ////

//// [unionOfArraysBooleanFilterCall.ts]
interface Fizz {
id: number;
member: number;
}
interface Buzz {
id: number;
member: string;
}
type Falsey = "" | 0 | false | null | undefined;


([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(Boolean); // expect type (Fizz|Buzz)[]

([] as (Fizz|Falsey)[] | readonly (Buzz|Falsey)[]).filter(Boolean); // expect type (Fizz|Buzz)[]

([] as [Fizz|Falsey] | readonly [(Buzz|Falsey)?]).filter(Boolean); // expect type (Fizz|Buzz)[]

// confirm that the other filter signatures are still available and working

declare function isFizz(x: unknown): x is Fizz;
([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(isFizz); // expect type Fizz[]

declare function isBuzz(x: unknown): x is Buzz;
([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(isBuzz); // expect type Buzz[]

([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(x => x && typeof x.member === "number"); // expect type (Fizz|Buzz|Falsey)[]


//// [unionOfArraysBooleanFilterCall.js]
[].filter(Boolean); // expect type (Fizz|Buzz)[]
[].filter(Boolean); // expect type (Fizz|Buzz)[]
[].filter(Boolean); // expect type (Fizz|Buzz)[]
[].filter(isFizz); // expect type Fizz[]
[].filter(isBuzz); // expect type Buzz[]
[].filter(x => x && typeof x.member === "number"); // expect type (Fizz|Buzz|Falsey)[]
97 changes: 97 additions & 0 deletions tests/baselines/reference/unionOfArraysBooleanFilterCall.symbols
@@ -0,0 +1,97 @@
//// [tests/cases/compiler/unionOfArraysBooleanFilterCall.ts] ////

=== unionOfArraysBooleanFilterCall.ts ===
interface Fizz {
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))

id: number;
>id : Symbol(Fizz.id, Decl(unionOfArraysBooleanFilterCall.ts, 0, 16))

member: number;
>member : Symbol(Fizz.member, Decl(unionOfArraysBooleanFilterCall.ts, 1, 15))
}
interface Buzz {
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))

id: number;
>id : Symbol(Buzz.id, Decl(unionOfArraysBooleanFilterCall.ts, 4, 16))

member: string;
>member : Symbol(Buzz.member, Decl(unionOfArraysBooleanFilterCall.ts, 5, 15))
}
type Falsey = "" | 0 | false | null | undefined;
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))


([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(Boolean); // expect type (Fizz|Buzz)[]
>([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Boolean : Symbol(Boolean, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

([] as (Fizz|Falsey)[] | readonly (Buzz|Falsey)[]).filter(Boolean); // expect type (Fizz|Buzz)[]
>([] as (Fizz|Falsey)[] | readonly (Buzz|Falsey)[]).filter : Symbol(filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>filter : Symbol(filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Boolean : Symbol(Boolean, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

([] as [Fizz|Falsey] | readonly [(Buzz|Falsey)?]).filter(Boolean); // expect type (Fizz|Buzz)[]
>([] as [Fizz|Falsey] | readonly [(Buzz|Falsey)?]).filter : Symbol(filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>filter : Symbol(filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Boolean : Symbol(Boolean, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

// confirm that the other filter signatures are still available and working

declare function isFizz(x: unknown): x is Fizz;
>isFizz : Symbol(isFizz, Decl(unionOfArraysBooleanFilterCall.ts, 15, 66))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 19, 24))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 19, 24))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))

([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(isFizz); // expect type Fizz[]
>([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>isFizz : Symbol(isFizz, Decl(unionOfArraysBooleanFilterCall.ts, 15, 66))

declare function isBuzz(x: unknown): x is Buzz;
>isBuzz : Symbol(isBuzz, Decl(unionOfArraysBooleanFilterCall.ts, 20, 57))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 22, 24))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 22, 24))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))

([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(isBuzz); // expect type Buzz[]
>([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>isBuzz : Symbol(isBuzz, Decl(unionOfArraysBooleanFilterCall.ts, 20, 57))

([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter(x => x && typeof x.member === "number"); // expect type (Fizz|Buzz|Falsey)[]
>([] as (Fizz|Falsey)[] | (Buzz|Falsey)[]).filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>Fizz : Symbol(Fizz, Decl(unionOfArraysBooleanFilterCall.ts, 0, 0))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>Buzz : Symbol(Buzz, Decl(unionOfArraysBooleanFilterCall.ts, 3, 1))
>Falsey : Symbol(Falsey, Decl(unionOfArraysBooleanFilterCall.ts, 7, 1))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 25, 49))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 25, 49))
>x.member : Symbol(member, Decl(unionOfArraysBooleanFilterCall.ts, 1, 15), Decl(unionOfArraysBooleanFilterCall.ts, 5, 15))
>x : Symbol(x, Decl(unionOfArraysBooleanFilterCall.ts, 25, 49))
>member : Symbol(member, Decl(unionOfArraysBooleanFilterCall.ts, 1, 15), Decl(unionOfArraysBooleanFilterCall.ts, 5, 15))