Skip to content

Commit

Permalink
PR-50377-redo-filterBooleanOverload
Browse files Browse the repository at this point in the history
  • Loading branch information
craigphicks committed Nov 1, 2023
1 parent 3e12250 commit f49ff0c
Show file tree
Hide file tree
Showing 59 changed files with 1,061 additions and 539 deletions.
39 changes: 29 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14749,19 +14749,38 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* maps primitive types and type parameters are to their apparent types.
*/
function getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[] {
const result = getSignaturesOfStructuredType(getReducedApparentType(type), kind);
if (kind === SignatureKind.Call && !length(result) && type.flags & TypeFlags.Union) {
function carveoutResult(type: Type, kind: SignatureKind): readonly Signature[] | undefined {
if (kind === SignatureKind.Call && type.flags & TypeFlags.Union) {
// If the union is all different instantiations of a member of the global array type...
let memberName: __String;
if (everyType(type, t => !!t.symbol?.parent && isArrayOrTupleSymbol(t.symbol.parent) && (!memberName ? (memberName = t.symbol.escapedName, true) : memberName === t.symbol.escapedName))) {
if ((type as UnionType).arrayFallbackSignatures) {
return (type as UnionType).arrayFallbackSignatures!;
}
const arrayArg = mapType(type, t => getMappedType((isReadonlyArraySymbol(t.symbol.parent) ? globalReadonlyArrayType : globalArrayType).typeParameters![0], (t as AnonymousType).mapper!));
const arrayType = createArrayType(arrayArg, someType(type, t => isReadonlyArraySymbol(t.symbol.parent)));
return (type as UnionType).arrayFallbackSignatures = getSignaturesOfType(getTypeOfPropertyOfType(arrayType, memberName!)!, kind);
}
}
return undefined;
}
let result = carveoutResult(type, kind);
if (result) return result;
// The original logical for the non-carveout case is recorded in the block comment below, for the record.
//result = getSignaturesOfStructuredType(getReducedApparentType(type), kind);
// if (kind === SignatureKind.Call && !length(result) && type.flags & TypeFlags.Union) {
// if ((type as UnionType).arrayFallbackSignatures) {
// return (type as UnionType).arrayFallbackSignatures!;
// }
// return (type as UnionType).arrayFallbackSignatures = result;
// }
if (kind === SignatureKind.Call && /*!length(result) && */ type.flags & TypeFlags.Union) {
if ((type as UnionType).arrayFallbackSignatures) {
return (type as UnionType).arrayFallbackSignatures!;
}
// If the union is all different instantiations of a member of the global array type...
let memberName: __String;
if (everyType(type, t => !!t.symbol?.parent && isArrayOrTupleSymbol(t.symbol.parent) && (!memberName ? (memberName = t.symbol.escapedName, true) : memberName === t.symbol.escapedName))) {
// Transform the type from `(A[] | B[])["member"]` to `(A | B)[]["member"]` (since we pretend array is covariant anyway)
const arrayArg = mapType(type, t => getMappedType((isReadonlyArraySymbol(t.symbol.parent) ? globalReadonlyArrayType : globalArrayType).typeParameters![0], (t as AnonymousType).mapper!));
const arrayType = createArrayType(arrayArg, someType(type, t => isReadonlyArraySymbol(t.symbol.parent)));
return (type as UnionType).arrayFallbackSignatures = getSignaturesOfType(getTypeOfPropertyOfType(arrayType, memberName!)!, kind);
}
}
result = getSignaturesOfStructuredType(getReducedApparentType(type), kind);
if (kind === SignatureKind.Call && /*!length(result) && */ type.flags & TypeFlags.Union) {
(type as UnionType).arrayFallbackSignatures = result;
}
return result;
Expand Down
1 change: 1 addition & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,7 @@ export namespace Completion {
interfaceEntry("StringConstructor"),
varEntry("Boolean"),
interfaceEntry("BooleanConstructor"),
interfaceEntry("BooleanConverter"),
varEntry("Number"),
interfaceEntry("NumberConstructor"),
interfaceEntry("TemplateStringsArray"),
Expand Down
17 changes: 17 additions & 0 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ interface BooleanConstructor {
readonly prototype: Boolean;
}

interface BooleanConverter {
<T>(value?: T): boolean;
readonly prototype: Boolean;
}

declare var Boolean: BooleanConstructor;

interface Number {
Expand Down Expand Up @@ -1257,6 +1262,12 @@ interface ReadonlyArray<T> {
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): S[];
/**
* Returns the non-Falsy elements of an array
* @param predicate Must be exactly "Boolean"
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter(predicate: BooleanConverter, thisArg?: any): (T extends false | 0 | "" | null | undefined | 0n ? never : T)[];
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
Expand Down Expand Up @@ -1448,6 +1459,12 @@ interface Array<T> {
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
/**
* Returns the non-Falsy elements of an array
* @param predicate Must be exactly "Boolean"
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter(predicate: BooleanConverter, thisArg?: any): (T extends false | 0 | "" | null | undefined | 0n ? never : T)[];
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/arrayFilter.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ var foo = [
]

foo.filter(x => x.name); //should accepted all possible types not only boolean!
>foo.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>foo.filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>foo : Symbol(foo, Decl(arrayFilter.ts, 0, 3))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>filter : Symbol(Array.filter, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>x : Symbol(x, Decl(arrayFilter.ts, 6, 11))
>x.name : Symbol(name, Decl(arrayFilter.ts, 1, 5))
>x : Symbol(x, Decl(arrayFilter.ts, 6, 11))
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/arrayFilter.types
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ var foo = [

foo.filter(x => x.name); //should accepted all possible types not only boolean!
>foo.filter(x => x.name) : { name: string; }[]
>foo.filter : { <S extends { name: string; }>(predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => value is S, thisArg?: any): S[]; (predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => unknown, thisArg?: any): { name: string; }[]; }
>foo.filter : { <S extends { name: string; }>(predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => value is S, thisArg?: any): S[]; (predicate: BooleanConverter, thisArg?: any): { name: string; }[]; (predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => unknown, thisArg?: any): { name: string; }[]; }
>foo : { name: string; }[]
>filter : { <S extends { name: string; }>(predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => value is S, thisArg?: any): S[]; (predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => unknown, thisArg?: any): { name: string; }[]; }
>filter : { <S extends { name: string; }>(predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => value is S, thisArg?: any): S[]; (predicate: BooleanConverter, thisArg?: any): { name: string; }[]; (predicate: (value: { name: string; }, index: number, array: { name: string; }[]) => unknown, thisArg?: any): { name: string; }[]; }
>x => x.name : (x: { name: string; }) => string
>x : { name: string; }
>x.name : string
Expand Down
32 changes: 32 additions & 0 deletions tests/baselines/reference/arrayFilterBooleanOverload.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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, --, --), 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, --, --), 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, --, --), 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, --, --), 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, --, --), 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, --, --), 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
Original file line number Diff line number Diff line change
@@ -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: BooleanConverter, thisArg?: any): string[]; (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: BooleanConverter, thisArg?: any): string[]; (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: BooleanConverter, thisArg?: any): string[]; (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: BooleanConverter, thisArg?: any): string[]; (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: BooleanConverter, thisArg?: any): (1 | "foo")[]; (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: BooleanConverter, thisArg?: any): (1 | "foo")[]; (predicate: (value: "" | 0 | 1 | "foo" | null, index: number, array: readonly ("" | 0 | 1 | "foo" | null)[]) => unknown, thisArg?: any): ("" | 0 | 1 | "foo" | null)[]; }
>Boolean : BooleanConstructor


2 changes: 1 addition & 1 deletion tests/baselines/reference/arraySlice.types
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ var arr: string[] | number[];
>arr : string[] | number[]

arr.splice(1, 1);
>arr.splice(1, 1) : string[] | number[]
>arr.splice(1, 1) : (string | number)[]
>arr.splice : { (start: number, deleteCount?: number): string[]; (start: number, deleteCount: number, ...items: string[]): string[]; } | { (start: number, deleteCount?: number): number[]; (start: number, deleteCount: number, ...items: number[]): number[]; }
>arr : string[] | number[]
>splice : { (start: number, deleteCount?: number): string[]; (start: number, deleteCount: number, ...items: string[]): string[]; } | { (start: number, deleteCount?: number): number[]; (start: number, deleteCount: number, ...items: number[]): number[]; }
Expand Down
6 changes: 6 additions & 0 deletions tests/baselines/reference/bestChoiceType.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
>match : Symbol(String.match, Decl(lib.es5.d.ts, --, --))
>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>s : Symbol(s, Decl(bestChoiceType.ts, 2, 26))
>s.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>s : Symbol(s, Decl(bestChoiceType.ts, 2, 26))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))

// Similar cases

Expand All @@ -31,7 +33,9 @@ function f1() {
>y : Symbol(y, Decl(bestChoiceType.ts, 8, 7))
>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>s : Symbol(s, Decl(bestChoiceType.ts, 9, 18))
>s.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>s : Symbol(s, Decl(bestChoiceType.ts, 9, 18))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
}

function f2() {
Expand All @@ -53,6 +57,8 @@ function f2() {
>y : Symbol(y, Decl(bestChoiceType.ts, 14, 7))
>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>s : Symbol(s, Decl(bestChoiceType.ts, 15, 18))
>s.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>s : Symbol(s, Decl(bestChoiceType.ts, 15, 18))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
}

Loading

0 comments on commit f49ff0c

Please sign in to comment.