From 17ed7e2edb380102bb2e74289dc38b3befdb968a Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Sat, 14 Sep 2019 11:44:18 +0100 Subject: [PATCH] Unify logic in typeof narrowing --- src/compiler/checker.ts | 186 ++++++++---------- .../reference/narrowingByTypeofInSwitch.js | 46 +++++ .../narrowingByTypeofInSwitch.symbols | 47 +++++ .../reference/narrowingByTypeofInSwitch.types | 66 +++++++ .../compiler/narrowingByTypeofInSwitch.ts | 17 +- 5 files changed, 257 insertions(+), 105 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f8c8817888591..0f1bc7df0e3a5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17491,110 +17491,28 @@ namespace ts { return type; } - function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type { - // We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands - const target = getReferenceCandidate(typeOfExpr.expression); - if (!isMatchingReference(reference, target)) { - // For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the - // narrowed type of 'y' to its declared type. - if (containsMatchingReference(reference, target)) { - return declaredType; - } - return type; - } - if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { - assumeTrue = !assumeTrue; - } - if (type.flags & TypeFlags.Any && literal.text === "function") { - return type; - } - const facts = assumeTrue ? - typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : - typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; - return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts); - - function narrowTypeForTypeof(type: Type) { - 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(type, targetType)) { - return type; - } - if (isTypeSubtypeOf(targetType, type)) { - return targetType; - } - 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) { - // We only narrow if all case expressions specify - // values with unit types, except for the case where - // `type` is unknown. In this instance we map object - // types to the nonPrimitive type and narrow with that. - const switchTypes = getSwitchClauseTypes(switchStatement); - if (!switchTypes.length) { - return type; - } - const clauseTypes = switchTypes.slice(clauseStart, clauseEnd); - const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType); - if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) { - let groundClauseTypes: Type[] | undefined; - for (let i = 0; i < clauseTypes.length; i += 1) { - const t = clauseTypes[i]; - if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) { - if (groundClauseTypes !== undefined) { - groundClauseTypes.push(t); - } - } - else if (t.flags & TypeFlags.Object) { - if (groundClauseTypes === undefined) { - groundClauseTypes = clauseTypes.slice(0, i); - } - groundClauseTypes.push(nonPrimitiveType); - } - else { - return type; - } - } - return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes); - } - const discriminantType = getUnionType(clauseTypes); - const caseType = - discriminantType.flags & TypeFlags.Never ? neverType : - replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType); - if (!hasDefaultClause) { - return caseType; - } - const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t)))); - return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); - } - - function getImpliedTypeFromTypeofCase(type: Type, text: string) { + function getImpliedTypeFromTypeofGuard(type: Type, text: string) { switch (text) { case "function": return type.flags & TypeFlags.Any ? type : globalFunctionType; case "object": return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type; default: - return typeofTypesByName.get(text) || type; + return typeofTypesByName.get(text); } } - function narrowTypeForTypeofSwitch(candidate: Type) { + // When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are + // super-types of the implied guard will be retained in the final type: this is because type-facts only + // filter. Instead, we would like to replace those union constituents with the more precise type implied by + // the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not + // the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to + // filtering by type-facts. + function narrowUnionMemberByTypeof(candidate: Type) { return (type: Type) => { + if (isTypeSubtypeOf(type, candidate)) { + return type; + } if (isTypeSubtypeOf(candidate, type)) { return candidate; } @@ -17608,6 +17526,30 @@ namespace ts { }; } + function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type { + // We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands + const target = getReferenceCandidate(typeOfExpr.expression); + if (!isMatchingReference(reference, target)) { + // For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the + // narrowed type of 'y' to its declared type. + if (containsMatchingReference(reference, target)) { + return declaredType; + } + return type; + } + if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + if (type.flags & TypeFlags.Any && literal.text === "function") { + return type; + } + const facts = assumeTrue ? + typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : + typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; + const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text); + return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts); + } + function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type { const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement); if (!switchWitnesses.length) { @@ -17619,11 +17561,9 @@ namespace ts { let clauseWitnesses: string[]; let switchFacts: TypeFacts; if (defaultCaseLocation > -1) { - // We no longer need the undefined denoting an - // explicit default case. Remove the undefined and - // fix-up clauseStart and clauseEnd. This means - // that we don't have to worry about undefined - // in the witness array. + // We no longer need the undefined denoting an explicit default case. Remove the undefined and + // fix-up clauseStart and clauseEnd. This means that we don't have to worry about undefined in the + // witness array. const witnesses = switchWitnesses.filter(witness => witness !== undefined); // The adjusted clause start and end after removing the `default` statement. const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart; @@ -17666,11 +17606,51 @@ namespace ts { boolean. We know that number cannot be selected because it is caught in the first clause. */ - let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts); - if (impliedType.flags & TypeFlags.Union) { - impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type)); + const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts); + return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts); + } + + function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) { + // We only narrow if all case expressions specify + // values with unit types, except for the case where + // `type` is unknown. In this instance we map object + // types to the nonPrimitive type and narrow with that. + const switchTypes = getSwitchClauseTypes(switchStatement); + if (!switchTypes.length) { + return type; + } + const clauseTypes = switchTypes.slice(clauseStart, clauseEnd); + const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType); + if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) { + let groundClauseTypes: Type[] | undefined; + for (let i = 0; i < clauseTypes.length; i += 1) { + const t = clauseTypes[i]; + if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) { + if (groundClauseTypes !== undefined) { + groundClauseTypes.push(t); + } + } + else if (t.flags & TypeFlags.Object) { + if (groundClauseTypes === undefined) { + groundClauseTypes = clauseTypes.slice(0, i); + } + groundClauseTypes.push(nonPrimitiveType); + } + else { + return type; + } + } + return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes); } - return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts); + const discriminantType = getUnionType(clauseTypes); + const caseType = + discriminantType.flags & TypeFlags.Never ? neverType : + replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType); + if (!hasDefaultClause) { + return caseType; + } + const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t)))); + return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { diff --git a/tests/baselines/reference/narrowingByTypeofInSwitch.js b/tests/baselines/reference/narrowingByTypeofInSwitch.js index 2b99da05e1357..bc203ff406f4a 100644 --- a/tests/baselines/reference/narrowingByTypeofInSwitch.js +++ b/tests/baselines/reference/narrowingByTypeofInSwitch.js @@ -249,9 +249,24 @@ function narrowingNarrows(x: {} | undefined) { default: const _y: {} = x; return; } } + +function narrowingNarrows2(x: true | 3 | 'hello' | undefined) { + switch (typeof x) { + case 'number': assertNumber(x); return; + case 'boolean': assertBoolean(x); return; + case 'function': assertNever(x); return; + case 'symbol': assertNever(x); return; + case 'object': const _: {} = assertNever(x); return; + case 'string': assertString(x); return; + case 'undefined': assertUndefined(x); return; + case 'number': assertNever(x); return; + default: const _y: {} = assertNever(x); return; + } +} //// [narrowingByTypeofInSwitch.js] +"use strict"; function assertNever(x) { return x; } @@ -585,3 +600,34 @@ function narrowingNarrows(x) { return; } } +function narrowingNarrows2(x) { + switch (typeof x) { + case 'number': + assertNumber(x); + return; + case 'boolean': + assertBoolean(x); + return; + case 'function': + assertNever(x); + return; + case 'symbol': + assertNever(x); + return; + case 'object': + var _ = assertNever(x); + return; + case 'string': + assertString(x); + return; + case 'undefined': + assertUndefined(x); + return; + case 'number': + assertNever(x); + return; + default: + var _y = assertNever(x); + return; + } +} diff --git a/tests/baselines/reference/narrowingByTypeofInSwitch.symbols b/tests/baselines/reference/narrowingByTypeofInSwitch.symbols index f1ff39725b39f..610bdb3128df1 100644 --- a/tests/baselines/reference/narrowingByTypeofInSwitch.symbols +++ b/tests/baselines/reference/narrowingByTypeofInSwitch.symbols @@ -715,3 +715,50 @@ function narrowingNarrows(x: {} | undefined) { } } +function narrowingNarrows2(x: true | 3 | 'hello' | undefined) { +>narrowingNarrows2 : Symbol(narrowingNarrows2, Decl(narrowingByTypeofInSwitch.ts, 249, 1)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + switch (typeof x) { +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'number': assertNumber(x); return; +>assertNumber : Symbol(assertNumber, Decl(narrowingByTypeofInSwitch.ts, 2, 1)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'boolean': assertBoolean(x); return; +>assertBoolean : Symbol(assertBoolean, Decl(narrowingByTypeofInSwitch.ts, 6, 1)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'function': assertNever(x); return; +>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'symbol': assertNever(x); return; +>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'object': const _: {} = assertNever(x); return; +>_ : Symbol(_, Decl(narrowingByTypeofInSwitch.ts, 257, 28)) +>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'string': assertString(x); return; +>assertString : Symbol(assertString, Decl(narrowingByTypeofInSwitch.ts, 10, 1)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'undefined': assertUndefined(x); return; +>assertUndefined : Symbol(assertUndefined, Decl(narrowingByTypeofInSwitch.ts, 30, 1)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + case 'number': assertNever(x); return; +>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + + default: const _y: {} = assertNever(x); return; +>_y : Symbol(_y, Decl(narrowingByTypeofInSwitch.ts, 261, 22)) +>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27)) + } +} + diff --git a/tests/baselines/reference/narrowingByTypeofInSwitch.types b/tests/baselines/reference/narrowingByTypeofInSwitch.types index f823c57770c3a..9e87fd421c5ae 100644 --- a/tests/baselines/reference/narrowingByTypeofInSwitch.types +++ b/tests/baselines/reference/narrowingByTypeofInSwitch.types @@ -869,3 +869,69 @@ function narrowingNarrows(x: {} | undefined) { } } +function narrowingNarrows2(x: true | 3 | 'hello' | undefined) { +>narrowingNarrows2 : (x: true | 3 | "hello" | undefined) => void +>x : true | 3 | "hello" | undefined +>true : true + + switch (typeof x) { +>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>x : true | 3 | "hello" | undefined + + case 'number': assertNumber(x); return; +>'number' : "number" +>assertNumber(x) : number +>assertNumber : (x: number) => number +>x : 3 + + case 'boolean': assertBoolean(x); return; +>'boolean' : "boolean" +>assertBoolean(x) : boolean +>assertBoolean : (x: boolean) => boolean +>x : true + + case 'function': assertNever(x); return; +>'function' : "function" +>assertNever(x) : never +>assertNever : (x: never) => never +>x : never + + case 'symbol': assertNever(x); return; +>'symbol' : "symbol" +>assertNever(x) : never +>assertNever : (x: never) => never +>x : never + + case 'object': const _: {} = assertNever(x); return; +>'object' : "object" +>_ : {} +>assertNever(x) : never +>assertNever : (x: never) => never +>x : never + + case 'string': assertString(x); return; +>'string' : "string" +>assertString(x) : string +>assertString : (x: string) => string +>x : "hello" + + case 'undefined': assertUndefined(x); return; +>'undefined' : "undefined" +>assertUndefined(x) : undefined +>assertUndefined : (x: undefined) => undefined +>x : undefined + + case 'number': assertNever(x); return; +>'number' : "number" +>assertNever(x) : never +>assertNever : (x: never) => never +>x : never + + default: const _y: {} = assertNever(x); return; +>_y : {} +>assertNever(x) : never +>assertNever : (x: never) => never +>x : never + } +} + diff --git a/tests/cases/compiler/narrowingByTypeofInSwitch.ts b/tests/cases/compiler/narrowingByTypeofInSwitch.ts index 39f04168684d1..b0f3362e1938d 100644 --- a/tests/cases/compiler/narrowingByTypeofInSwitch.ts +++ b/tests/cases/compiler/narrowingByTypeofInSwitch.ts @@ -1,5 +1,4 @@ -// @strictNullChecks: true -// @strictFunctionTypes: true +// @strict: true function assertNever(x: never) { return x; @@ -251,3 +250,17 @@ function narrowingNarrows(x: {} | undefined) { default: const _y: {} = x; return; } } + +function narrowingNarrows2(x: true | 3 | 'hello' | undefined) { + switch (typeof x) { + case 'number': assertNumber(x); return; + case 'boolean': assertBoolean(x); return; + case 'function': assertNever(x); return; + case 'symbol': assertNever(x); return; + case 'object': const _: {} = assertNever(x); return; + case 'string': assertString(x); return; + case 'undefined': assertUndefined(x); return; + case 'number': assertNever(x); return; + default: const _y: {} = assertNever(x); return; + } +}