Change typeof narrowing to narrow selected union members (#25243)

* For typeof narrow all union members prior to filtering

* Revise narrowTypeByTypeof to both narrow unions and applicable union members

* Add repros from issue
This commit is contained in:
Wesley Wigham 2018-09-06 00:41:09 -07:00 committed by GitHub
parent c62920ac81
commit d8f736d319
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 48 deletions

View file

@ -14963,30 +14963,34 @@ namespace ts {
if (type.flags & TypeFlags.Any && literal.text === "function") {
return type;
}
if (assumeTrue && !(type.flags & TypeFlags.Union)) {
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
return getUnionType([nonPrimitiveType, nullType]);
}
// We narrow a non-union type to an exact primitive type if the non-union type
// is a supertype of that primitive type. For example, type 'any' can be narrowed
// to one of the primitive types.
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
if (targetType) {
if (isTypeSubtypeOf(targetType, type)) {
return targetType;
}
if (type.flags & TypeFlags.Instantiable) {
const constraint = getBaseConstraintOfType(type) || anyType;
if (isTypeSubtypeOf(targetType, constraint)) {
return getIntersectionType([type, targetType]);
}
}
}
}
const facts = assumeTrue ?
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
return getTypeWithFacts(type, facts);
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
function narrowTypeForTypeof(type: Type) {
if (assumeTrue && !(type.flags & TypeFlags.Union)) {
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
return getUnionType([nonPrimitiveType, nullType]);
}
// We narrow a non-union type to an exact primitive type if the non-union type
// is a supertype of that primitive type. For example, type 'any' can be narrowed
// to one of the primitive types.
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
if (targetType) {
if (isTypeSubtypeOf(targetType, type)) {
return isTypeAny(type) ? targetType : getIntersectionType([type, targetType]); // Intersection to handle `string` being a subtype of `keyof T`
}
if (type.flags & TypeFlags.Instantiable) {
const constraint = getBaseConstraintOfType(type) || anyType;
if (isTypeSubtypeOf(targetType, constraint)) {
return getIntersectionType([type, targetType]);
}
}
}
}
return type;
}
}
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {

View file

@ -104,7 +104,7 @@ function c<T>(data: string | T): T {
>JSON.parse : (text: string, reviver?: (key: any, value: any) => any) => any
>JSON : JSON
>parse : (text: string, reviver?: (key: any, value: any) => any) => any
>data : string
>data : string | (T & string)
}
else {
return data;

View file

@ -58,9 +58,9 @@ export function css<S extends { [K in keyof S]: string }>(styles: S, ...classNam
>"string" : "string"
return styles[arg];
>styles[arg] : S[keyof S]
>styles[arg] : S[keyof S & string]
>styles : S
>arg : keyof S
>arg : keyof S & string
}
if (typeof arg == "object") {
>typeof arg == "object" : boolean

View file

@ -0,0 +1,32 @@
//// [strictTypeofUnionNarrowing.ts]
function stringify1(anything: { toString(): string } | undefined): string {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify2(anything: {} | undefined): string {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify4(anything: { toString?(): string } | undefined): string {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
//// [strictTypeofUnionNarrowing.js]
"use strict";
function stringify1(anything) {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify2(anything) {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify3(anything) {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify4(anything) {
return typeof anything === "string" ? anything.toUpperCase() : "";
}

View file

@ -0,0 +1,47 @@
=== tests/cases/compiler/strictTypeofUnionNarrowing.ts ===
function stringify1(anything: { toString(): string } | undefined): string {
>stringify1 : Symbol(stringify1, Decl(strictTypeofUnionNarrowing.ts, 0, 0))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 0, 20))
>toString : Symbol(toString, Decl(strictTypeofUnionNarrowing.ts, 0, 31))
return typeof anything === "string" ? anything.toUpperCase() : "";
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 0, 20))
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 0, 20))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
function stringify2(anything: {} | undefined): string {
>stringify2 : Symbol(stringify2, Decl(strictTypeofUnionNarrowing.ts, 2, 1))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 4, 20))
return typeof anything === "string" ? anything.toUpperCase() : "";
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 4, 20))
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 4, 20))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
>stringify3 : Symbol(stringify3, Decl(strictTypeofUnionNarrowing.ts, 6, 1))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 8, 20))
return typeof anything === "string" ? anything.toUpperCase() : "";
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 8, 20))
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 8, 20))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
function stringify4(anything: { toString?(): string } | undefined): string {
>stringify4 : Symbol(stringify4, Decl(strictTypeofUnionNarrowing.ts, 10, 1))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 12, 20))
>toString : Symbol(toString, Decl(strictTypeofUnionNarrowing.ts, 12, 31))
return typeof anything === "string" ? anything.toUpperCase() : "";
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 12, 20))
>anything.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>anything : Symbol(anything, Decl(strictTypeofUnionNarrowing.ts, 12, 20))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}

View file

@ -0,0 +1,71 @@
=== tests/cases/compiler/strictTypeofUnionNarrowing.ts ===
function stringify1(anything: { toString(): string } | undefined): string {
>stringify1 : (anything: { toString(): string; } | undefined) => string
>anything : { toString(): string; } | undefined
>toString : () => string
return typeof anything === "string" ? anything.toUpperCase() : "";
>typeof anything === "string" ? anything.toUpperCase() : "" : string
>typeof anything === "string" : boolean
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>anything : { toString(): string; } | undefined
>"string" : "string"
>anything.toUpperCase() : string
>anything.toUpperCase : () => string
>anything : { toString(): string; } & string
>toUpperCase : () => string
>"" : ""
}
function stringify2(anything: {} | undefined): string {
>stringify2 : (anything: {} | undefined) => string
>anything : {} | undefined
return typeof anything === "string" ? anything.toUpperCase() : "";
>typeof anything === "string" ? anything.toUpperCase() : "" : string
>typeof anything === "string" : boolean
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>anything : {} | undefined
>"string" : "string"
>anything.toUpperCase() : string
>anything.toUpperCase : () => string
>anything : string & {}
>toUpperCase : () => string
>"" : ""
}
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
>stringify3 : (anything: unknown) => string
>anything : unknown
return typeof anything === "string" ? anything.toUpperCase() : "";
>typeof anything === "string" ? anything.toUpperCase() : "" : string
>typeof anything === "string" : boolean
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>anything : unknown
>"string" : "string"
>anything.toUpperCase() : string
>anything.toUpperCase : () => string
>anything : string
>toUpperCase : () => string
>"" : ""
}
function stringify4(anything: { toString?(): string } | undefined): string {
>stringify4 : (anything: {} | undefined) => string
>anything : {} | undefined
>toString : (() => string) | undefined
return typeof anything === "string" ? anything.toUpperCase() : "";
>typeof anything === "string" ? anything.toUpperCase() : "" : string
>typeof anything === "string" : boolean
>typeof anything : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>anything : {} | undefined
>"string" : "string"
>anything.toUpperCase() : string
>anything.toUpperCase : () => string
>anything : {} & string
>toUpperCase : () => string
>"" : ""
}

View file

@ -14,7 +14,7 @@ if (typeof a === "number") {
let c: number = a;
>c : number
>a : number
>a : number & {}
}
if (typeof a === "string") {
>typeof a === "string" : boolean
@ -24,7 +24,7 @@ if (typeof a === "string") {
let c: string = a;
>c : string
>a : string
>a : string & {}
}
if (typeof a === "boolean") {
>typeof a === "boolean" : boolean
@ -34,7 +34,7 @@ if (typeof a === "boolean") {
let c: boolean = a;
>c : boolean
>a : boolean
>a : (false & {}) | (true & {})
}
if (typeof b === "number") {
@ -45,7 +45,7 @@ if (typeof b === "number") {
let c: number = b;
>c : number
>b : number
>b : { toString(): string; } & number
}
if (typeof b === "string") {
>typeof b === "string" : boolean
@ -55,7 +55,7 @@ if (typeof b === "string") {
let c: string = b;
>c : string
>b : string
>b : { toString(): string; } & string
}
if (typeof b === "boolean") {
>typeof b === "boolean" : boolean
@ -65,6 +65,6 @@ if (typeof b === "boolean") {
let c: boolean = b;
>c : boolean
>b : boolean
>b : ({ toString(): string; } & false) | ({ toString(): string; } & true)
}

View file

@ -134,7 +134,7 @@ function test5(a: boolean | void) {
}
else {
a;
>a : boolean | void
>a : undefined
}
}
@ -151,15 +151,15 @@ function test6(a: boolean | void) {
if (typeof a === "boolean") {
>typeof a === "boolean" : boolean
>typeof a : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a : boolean | void
>a : undefined
>"boolean" : "boolean"
a;
>a : boolean
>a : never
}
else {
a;
>a : void
>a : undefined
}
}
else {
@ -184,7 +184,7 @@ function test7(a: boolean | void) {
>"boolean" : "boolean"
a;
>a : boolean | void
>a : boolean
}
else {
a;
@ -212,7 +212,7 @@ function test8(a: boolean | void) {
}
else {
a;
>a : boolean | void
>a : undefined
}
}
@ -242,7 +242,7 @@ function test9(a: boolean | number) {
}
else {
a;
>a : number | boolean
>a : undefined
}
}
@ -259,15 +259,15 @@ function test10(a: boolean | number) {
if (typeof a === "boolean") {
>typeof a === "boolean" : boolean
>typeof a : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a : number | boolean
>a : undefined
>"boolean" : "boolean"
a;
>a : boolean
>a : never
}
else {
a;
>a : number
>a : undefined
}
}
else {
@ -292,7 +292,7 @@ function test11(a: boolean | number) {
>"boolean" : "boolean"
a;
>a : number | boolean
>a : boolean
}
else {
a;
@ -320,7 +320,7 @@ function test12(a: boolean | number) {
}
else {
a;
>a : number | boolean
>a : number
}
}
@ -350,7 +350,7 @@ function test13(a: boolean | number | void) {
}
else {
a;
>a : number | boolean | void
>a : undefined
}
}
@ -367,15 +367,15 @@ function test14(a: boolean | number | void) {
if (typeof a === "boolean") {
>typeof a === "boolean" : boolean
>typeof a : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a : number | boolean | void
>a : undefined
>"boolean" : "boolean"
a;
>a : boolean
>a : never
}
else {
a;
>a : number | void
>a : undefined
}
}
else {
@ -400,7 +400,7 @@ function test15(a: boolean | number | void) {
>"boolean" : "boolean"
a;
>a : number | boolean | void
>a : boolean
}
else {
a;
@ -428,7 +428,7 @@ function test16(a: boolean | number | void) {
}
else {
a;
>a : number | boolean | void
>a : number
}
}

View file

@ -0,0 +1,16 @@
// @strict: true
function stringify1(anything: { toString(): string } | undefined): string {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify2(anything: {} | undefined): string {
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify3(anything: unknown | undefined): string { // should simplify to just `unknown` which should narrow fine
return typeof anything === "string" ? anything.toUpperCase() : "";
}
function stringify4(anything: { toString?(): string } | undefined): string {
return typeof anything === "string" ? anything.toUpperCase() : "";
}