From bb3467b8e1c2b7897bc30e282e59bd85a8b8c714 Mon Sep 17 00:00:00 2001 From: Joe Calzaretta Date: Mon, 9 Oct 2017 17:58:41 -0400 Subject: [PATCH] Handle type guard predicates on `Array.find` (#18160) * Handle type guard predicates on `Array.find` If the `predicate` function passed to `Array.find` or `ReadonlyArray.find` is a type guard narrowing `value` to type `S`, then any returned element should also be narrowed to `S`. Adding test case and associated baselines * trailing whitespace after merge conflict --- src/lib/es2015.core.d.ts | 2 + tests/baselines/reference/arrayFind.js | 22 ++++++++++ tests/baselines/reference/arrayFind.symbols | 33 +++++++++++++++ tests/baselines/reference/arrayFind.types | 46 +++++++++++++++++++++ tests/cases/compiler/arrayFind.ts | 12 ++++++ 5 files changed, 115 insertions(+) create mode 100644 tests/baselines/reference/arrayFind.js create mode 100644 tests/baselines/reference/arrayFind.symbols create mode 100644 tests/baselines/reference/arrayFind.types create mode 100644 tests/cases/compiler/arrayFind.ts diff --git a/src/lib/es2015.core.d.ts b/src/lib/es2015.core.d.ts index 5c2438d9052..9ea773e3eef 100644 --- a/src/lib/es2015.core.d.ts +++ b/src/lib/es2015.core.d.ts @@ -10,6 +10,7 @@ interface Array { * @param thisArg If provided, it will be used as the this value for each invocation of * predicate. If it is not provided, undefined is used instead. */ + find(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): T | undefined; /** @@ -350,6 +351,7 @@ interface ReadonlyArray { * @param thisArg If provided, it will be used as the this value for each invocation of * predicate. If it is not provided, undefined is used instead. */ + find(predicate: (this: void, value: T, index: number, obj: ReadonlyArray) => value is S, thisArg?: any): S | undefined; find(predicate: (value: T, index: number, obj: ReadonlyArray) => boolean, thisArg?: any): T | undefined; /** diff --git a/tests/baselines/reference/arrayFind.js b/tests/baselines/reference/arrayFind.js new file mode 100644 index 00000000000..1926c3a8dcc --- /dev/null +++ b/tests/baselines/reference/arrayFind.js @@ -0,0 +1,22 @@ +//// [arrayFind.ts] +// test fix for #18112, type guard predicates should narrow returned element +function isNumber(x: any): x is number { + return typeof x === "number"; +} + +const arrayOfStringsNumbersAndBooleans = ["string", false, 0, "strung", 1, true]; +const foundNumber: number | undefined = arrayOfStringsNumbersAndBooleans.find(isNumber); + +const readonlyArrayOfStringsNumbersAndBooleans = arrayOfStringsNumbersAndBooleans as ReadonlyArray; +const readonlyFoundNumber: number | undefined = readonlyArrayOfStringsNumbersAndBooleans.find(isNumber); + + +//// [arrayFind.js] +// test fix for #18112, type guard predicates should narrow returned element +function isNumber(x) { + return typeof x === "number"; +} +var arrayOfStringsNumbersAndBooleans = ["string", false, 0, "strung", 1, true]; +var foundNumber = arrayOfStringsNumbersAndBooleans.find(isNumber); +var readonlyArrayOfStringsNumbersAndBooleans = arrayOfStringsNumbersAndBooleans; +var readonlyFoundNumber = readonlyArrayOfStringsNumbersAndBooleans.find(isNumber); diff --git a/tests/baselines/reference/arrayFind.symbols b/tests/baselines/reference/arrayFind.symbols new file mode 100644 index 00000000000..163d5d818ba --- /dev/null +++ b/tests/baselines/reference/arrayFind.symbols @@ -0,0 +1,33 @@ +=== tests/cases/compiler/arrayFind.ts === +// test fix for #18112, type guard predicates should narrow returned element +function isNumber(x: any): x is number { +>isNumber : Symbol(isNumber, Decl(arrayFind.ts, 0, 0)) +>x : Symbol(x, Decl(arrayFind.ts, 1, 18)) +>x : Symbol(x, Decl(arrayFind.ts, 1, 18)) + + return typeof x === "number"; +>x : Symbol(x, Decl(arrayFind.ts, 1, 18)) +} + +const arrayOfStringsNumbersAndBooleans = ["string", false, 0, "strung", 1, true]; +>arrayOfStringsNumbersAndBooleans : Symbol(arrayOfStringsNumbersAndBooleans, Decl(arrayFind.ts, 5, 5)) + +const foundNumber: number | undefined = arrayOfStringsNumbersAndBooleans.find(isNumber); +>foundNumber : Symbol(foundNumber, Decl(arrayFind.ts, 6, 5)) +>arrayOfStringsNumbersAndBooleans.find : Symbol(Array.find, Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --)) +>arrayOfStringsNumbersAndBooleans : Symbol(arrayOfStringsNumbersAndBooleans, Decl(arrayFind.ts, 5, 5)) +>find : Symbol(Array.find, Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --)) +>isNumber : Symbol(isNumber, Decl(arrayFind.ts, 0, 0)) + +const readonlyArrayOfStringsNumbersAndBooleans = arrayOfStringsNumbersAndBooleans as ReadonlyArray; +>readonlyArrayOfStringsNumbersAndBooleans : Symbol(readonlyArrayOfStringsNumbersAndBooleans, Decl(arrayFind.ts, 8, 5)) +>arrayOfStringsNumbersAndBooleans : Symbol(arrayOfStringsNumbersAndBooleans, Decl(arrayFind.ts, 5, 5)) +>ReadonlyArray : Symbol(ReadonlyArray, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --)) + +const readonlyFoundNumber: number | undefined = readonlyArrayOfStringsNumbersAndBooleans.find(isNumber); +>readonlyFoundNumber : Symbol(readonlyFoundNumber, Decl(arrayFind.ts, 9, 5)) +>readonlyArrayOfStringsNumbersAndBooleans.find : Symbol(ReadonlyArray.find, Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --)) +>readonlyArrayOfStringsNumbersAndBooleans : Symbol(readonlyArrayOfStringsNumbersAndBooleans, Decl(arrayFind.ts, 8, 5)) +>find : Symbol(ReadonlyArray.find, Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --)) +>isNumber : Symbol(isNumber, Decl(arrayFind.ts, 0, 0)) + diff --git a/tests/baselines/reference/arrayFind.types b/tests/baselines/reference/arrayFind.types new file mode 100644 index 00000000000..5c0769cb606 --- /dev/null +++ b/tests/baselines/reference/arrayFind.types @@ -0,0 +1,46 @@ +=== tests/cases/compiler/arrayFind.ts === +// test fix for #18112, type guard predicates should narrow returned element +function isNumber(x: any): x is number { +>isNumber : (x: any) => x is number +>x : any +>x : any + + return typeof x === "number"; +>typeof x === "number" : boolean +>typeof x : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : any +>"number" : "number" +} + +const arrayOfStringsNumbersAndBooleans = ["string", false, 0, "strung", 1, true]; +>arrayOfStringsNumbersAndBooleans : (string | number | boolean)[] +>["string", false, 0, "strung", 1, true] : (string | number | boolean)[] +>"string" : "string" +>false : false +>0 : 0 +>"strung" : "strung" +>1 : 1 +>true : true + +const foundNumber: number | undefined = arrayOfStringsNumbersAndBooleans.find(isNumber); +>foundNumber : number +>arrayOfStringsNumbersAndBooleans.find(isNumber) : number +>arrayOfStringsNumbersAndBooleans.find : { (predicate: (this: void, value: string | number | boolean, index: number, obj: (string | number | boolean)[]) => value is S, thisArg?: any): S; (predicate: (value: string | number | boolean, index: number, obj: (string | number | boolean)[]) => boolean, thisArg?: any): string | number | boolean; } +>arrayOfStringsNumbersAndBooleans : (string | number | boolean)[] +>find : { (predicate: (this: void, value: string | number | boolean, index: number, obj: (string | number | boolean)[]) => value is S, thisArg?: any): S; (predicate: (value: string | number | boolean, index: number, obj: (string | number | boolean)[]) => boolean, thisArg?: any): string | number | boolean; } +>isNumber : (x: any) => x is number + +const readonlyArrayOfStringsNumbersAndBooleans = arrayOfStringsNumbersAndBooleans as ReadonlyArray; +>readonlyArrayOfStringsNumbersAndBooleans : ReadonlyArray +>arrayOfStringsNumbersAndBooleans as ReadonlyArray : ReadonlyArray +>arrayOfStringsNumbersAndBooleans : (string | number | boolean)[] +>ReadonlyArray : ReadonlyArray + +const readonlyFoundNumber: number | undefined = readonlyArrayOfStringsNumbersAndBooleans.find(isNumber); +>readonlyFoundNumber : number +>readonlyArrayOfStringsNumbersAndBooleans.find(isNumber) : number +>readonlyArrayOfStringsNumbersAndBooleans.find : { (predicate: (this: void, value: string | number | boolean, index: number, obj: ReadonlyArray) => value is S, thisArg?: any): S; (predicate: (value: string | number | boolean, index: number, obj: ReadonlyArray) => boolean, thisArg?: any): string | number | boolean; } +>readonlyArrayOfStringsNumbersAndBooleans : ReadonlyArray +>find : { (predicate: (this: void, value: string | number | boolean, index: number, obj: ReadonlyArray) => value is S, thisArg?: any): S; (predicate: (value: string | number | boolean, index: number, obj: ReadonlyArray) => boolean, thisArg?: any): string | number | boolean; } +>isNumber : (x: any) => x is number + diff --git a/tests/cases/compiler/arrayFind.ts b/tests/cases/compiler/arrayFind.ts new file mode 100644 index 00000000000..90883974766 --- /dev/null +++ b/tests/cases/compiler/arrayFind.ts @@ -0,0 +1,12 @@ +// @lib: es2015 + +// test fix for #18112, type guard predicates should narrow returned element +function isNumber(x: any): x is number { + return typeof x === "number"; +} + +const arrayOfStringsNumbersAndBooleans = ["string", false, 0, "strung", 1, true]; +const foundNumber: number | undefined = arrayOfStringsNumbersAndBooleans.find(isNumber); + +const readonlyArrayOfStringsNumbersAndBooleans = arrayOfStringsNumbersAndBooleans as ReadonlyArray; +const readonlyFoundNumber: number | undefined = readonlyArrayOfStringsNumbersAndBooleans.find(isNumber);