Skip to content

Commit 17ed7e2

Browse files
committed
Unify logic in typeof narrowing
1 parent 038d951 commit 17ed7e2

File tree

5 files changed

+257
-105
lines changed

5 files changed

+257
-105
lines changed

src/compiler/checker.ts

Lines changed: 83 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -17491,110 +17491,28 @@ namespace ts {
1749117491
return type;
1749217492
}
1749317493

17494-
function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type {
17495-
// We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands
17496-
const target = getReferenceCandidate(typeOfExpr.expression);
17497-
if (!isMatchingReference(reference, target)) {
17498-
// For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the
17499-
// narrowed type of 'y' to its declared type.
17500-
if (containsMatchingReference(reference, target)) {
17501-
return declaredType;
17502-
}
17503-
return type;
17504-
}
17505-
if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
17506-
assumeTrue = !assumeTrue;
17507-
}
17508-
if (type.flags & TypeFlags.Any && literal.text === "function") {
17509-
return type;
17510-
}
17511-
const facts = assumeTrue ?
17512-
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
17513-
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
17514-
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
17515-
17516-
function narrowTypeForTypeof(type: Type) {
17517-
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
17518-
return getUnionType([nonPrimitiveType, nullType]);
17519-
}
17520-
// We narrow a non-union type to an exact primitive type if the non-union type
17521-
// is a supertype of that primitive type. For example, type 'any' can be narrowed
17522-
// to one of the primitive types.
17523-
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
17524-
if (targetType) {
17525-
if (isTypeSubtypeOf(type, targetType)) {
17526-
return type;
17527-
}
17528-
if (isTypeSubtypeOf(targetType, type)) {
17529-
return targetType;
17530-
}
17531-
if (type.flags & TypeFlags.Instantiable) {
17532-
const constraint = getBaseConstraintOfType(type) || anyType;
17533-
if (isTypeSubtypeOf(targetType, constraint)) {
17534-
return getIntersectionType([type, targetType]);
17535-
}
17536-
}
17537-
}
17538-
return type;
17539-
}
17540-
}
17541-
17542-
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
17543-
// We only narrow if all case expressions specify
17544-
// values with unit types, except for the case where
17545-
// `type` is unknown. In this instance we map object
17546-
// types to the nonPrimitive type and narrow with that.
17547-
const switchTypes = getSwitchClauseTypes(switchStatement);
17548-
if (!switchTypes.length) {
17549-
return type;
17550-
}
17551-
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
17552-
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType);
17553-
if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) {
17554-
let groundClauseTypes: Type[] | undefined;
17555-
for (let i = 0; i < clauseTypes.length; i += 1) {
17556-
const t = clauseTypes[i];
17557-
if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) {
17558-
if (groundClauseTypes !== undefined) {
17559-
groundClauseTypes.push(t);
17560-
}
17561-
}
17562-
else if (t.flags & TypeFlags.Object) {
17563-
if (groundClauseTypes === undefined) {
17564-
groundClauseTypes = clauseTypes.slice(0, i);
17565-
}
17566-
groundClauseTypes.push(nonPrimitiveType);
17567-
}
17568-
else {
17569-
return type;
17570-
}
17571-
}
17572-
return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes);
17573-
}
17574-
const discriminantType = getUnionType(clauseTypes);
17575-
const caseType =
17576-
discriminantType.flags & TypeFlags.Never ? neverType :
17577-
replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType);
17578-
if (!hasDefaultClause) {
17579-
return caseType;
17580-
}
17581-
const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t))));
17582-
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
17583-
}
17584-
17585-
function getImpliedTypeFromTypeofCase(type: Type, text: string) {
17494+
function getImpliedTypeFromTypeofGuard(type: Type, text: string) {
1758617495
switch (text) {
1758717496
case "function":
1758817497
return type.flags & TypeFlags.Any ? type : globalFunctionType;
1758917498
case "object":
1759017499
return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type;
1759117500
default:
17592-
return typeofTypesByName.get(text) || type;
17501+
return typeofTypesByName.get(text);
1759317502
}
1759417503
}
1759517504

17596-
function narrowTypeForTypeofSwitch(candidate: Type) {
17505+
// When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are
17506+
// super-types of the implied guard will be retained in the final type: this is because type-facts only
17507+
// filter. Instead, we would like to replace those union constituents with the more precise type implied by
17508+
// the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not
17509+
// the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to
17510+
// filtering by type-facts.
17511+
function narrowUnionMemberByTypeof(candidate: Type) {
1759717512
return (type: Type) => {
17513+
if (isTypeSubtypeOf(type, candidate)) {
17514+
return type;
17515+
}
1759817516
if (isTypeSubtypeOf(candidate, type)) {
1759917517
return candidate;
1760017518
}
@@ -17608,6 +17526,30 @@ namespace ts {
1760817526
};
1760917527
}
1761017528

17529+
function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type {
17530+
// We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands
17531+
const target = getReferenceCandidate(typeOfExpr.expression);
17532+
if (!isMatchingReference(reference, target)) {
17533+
// For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the
17534+
// narrowed type of 'y' to its declared type.
17535+
if (containsMatchingReference(reference, target)) {
17536+
return declaredType;
17537+
}
17538+
return type;
17539+
}
17540+
if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
17541+
assumeTrue = !assumeTrue;
17542+
}
17543+
if (type.flags & TypeFlags.Any && literal.text === "function") {
17544+
return type;
17545+
}
17546+
const facts = assumeTrue ?
17547+
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
17548+
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
17549+
const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text);
17550+
return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts);
17551+
}
17552+
1761117553
function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
1761217554
const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement);
1761317555
if (!switchWitnesses.length) {
@@ -17619,11 +17561,9 @@ namespace ts {
1761917561
let clauseWitnesses: string[];
1762017562
let switchFacts: TypeFacts;
1762117563
if (defaultCaseLocation > -1) {
17622-
// We no longer need the undefined denoting an
17623-
// explicit default case. Remove the undefined and
17624-
// fix-up clauseStart and clauseEnd. This means
17625-
// that we don't have to worry about undefined
17626-
// in the witness array.
17564+
// We no longer need the undefined denoting an explicit default case. Remove the undefined and
17565+
// fix-up clauseStart and clauseEnd. This means that we don't have to worry about undefined in the
17566+
// witness array.
1762717567
const witnesses = <string[]>switchWitnesses.filter(witness => witness !== undefined);
1762817568
// The adjusted clause start and end after removing the `default` statement.
1762917569
const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart;
@@ -17666,11 +17606,51 @@ namespace ts {
1766617606
boolean. We know that number cannot be selected
1766717607
because it is caught in the first clause.
1766817608
*/
17669-
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts);
17670-
if (impliedType.flags & TypeFlags.Union) {
17671-
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type));
17609+
const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts);
17610+
return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts);
17611+
}
17612+
17613+
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
17614+
// We only narrow if all case expressions specify
17615+
// values with unit types, except for the case where
17616+
// `type` is unknown. In this instance we map object
17617+
// types to the nonPrimitive type and narrow with that.
17618+
const switchTypes = getSwitchClauseTypes(switchStatement);
17619+
if (!switchTypes.length) {
17620+
return type;
17621+
}
17622+
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
17623+
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType);
17624+
if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) {
17625+
let groundClauseTypes: Type[] | undefined;
17626+
for (let i = 0; i < clauseTypes.length; i += 1) {
17627+
const t = clauseTypes[i];
17628+
if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) {
17629+
if (groundClauseTypes !== undefined) {
17630+
groundClauseTypes.push(t);
17631+
}
17632+
}
17633+
else if (t.flags & TypeFlags.Object) {
17634+
if (groundClauseTypes === undefined) {
17635+
groundClauseTypes = clauseTypes.slice(0, i);
17636+
}
17637+
groundClauseTypes.push(nonPrimitiveType);
17638+
}
17639+
else {
17640+
return type;
17641+
}
17642+
}
17643+
return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes);
1767217644
}
17673-
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
17645+
const discriminantType = getUnionType(clauseTypes);
17646+
const caseType =
17647+
discriminantType.flags & TypeFlags.Never ? neverType :
17648+
replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType);
17649+
if (!hasDefaultClause) {
17650+
return caseType;
17651+
}
17652+
const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t))));
17653+
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
1767417654
}
1767517655

1767617656
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {

tests/baselines/reference/narrowingByTypeofInSwitch.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,24 @@ function narrowingNarrows(x: {} | undefined) {
249249
default: const _y: {} = x; return;
250250
}
251251
}
252+
253+
function narrowingNarrows2(x: true | 3 | 'hello' | undefined) {
254+
switch (typeof x) {
255+
case 'number': assertNumber(x); return;
256+
case 'boolean': assertBoolean(x); return;
257+
case 'function': assertNever(x); return;
258+
case 'symbol': assertNever(x); return;
259+
case 'object': const _: {} = assertNever(x); return;
260+
case 'string': assertString(x); return;
261+
case 'undefined': assertUndefined(x); return;
262+
case 'number': assertNever(x); return;
263+
default: const _y: {} = assertNever(x); return;
264+
}
265+
}
252266

253267

254268
//// [narrowingByTypeofInSwitch.js]
269+
"use strict";
255270
function assertNever(x) {
256271
return x;
257272
}
@@ -585,3 +600,34 @@ function narrowingNarrows(x) {
585600
return;
586601
}
587602
}
603+
function narrowingNarrows2(x) {
604+
switch (typeof x) {
605+
case 'number':
606+
assertNumber(x);
607+
return;
608+
case 'boolean':
609+
assertBoolean(x);
610+
return;
611+
case 'function':
612+
assertNever(x);
613+
return;
614+
case 'symbol':
615+
assertNever(x);
616+
return;
617+
case 'object':
618+
var _ = assertNever(x);
619+
return;
620+
case 'string':
621+
assertString(x);
622+
return;
623+
case 'undefined':
624+
assertUndefined(x);
625+
return;
626+
case 'number':
627+
assertNever(x);
628+
return;
629+
default:
630+
var _y = assertNever(x);
631+
return;
632+
}
633+
}

tests/baselines/reference/narrowingByTypeofInSwitch.symbols

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,3 +715,50 @@ function narrowingNarrows(x: {} | undefined) {
715715
}
716716
}
717717

718+
function narrowingNarrows2(x: true | 3 | 'hello' | undefined) {
719+
>narrowingNarrows2 : Symbol(narrowingNarrows2, Decl(narrowingByTypeofInSwitch.ts, 249, 1))
720+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
721+
722+
switch (typeof x) {
723+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
724+
725+
case 'number': assertNumber(x); return;
726+
>assertNumber : Symbol(assertNumber, Decl(narrowingByTypeofInSwitch.ts, 2, 1))
727+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
728+
729+
case 'boolean': assertBoolean(x); return;
730+
>assertBoolean : Symbol(assertBoolean, Decl(narrowingByTypeofInSwitch.ts, 6, 1))
731+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
732+
733+
case 'function': assertNever(x); return;
734+
>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0))
735+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
736+
737+
case 'symbol': assertNever(x); return;
738+
>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0))
739+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
740+
741+
case 'object': const _: {} = assertNever(x); return;
742+
>_ : Symbol(_, Decl(narrowingByTypeofInSwitch.ts, 257, 28))
743+
>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0))
744+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
745+
746+
case 'string': assertString(x); return;
747+
>assertString : Symbol(assertString, Decl(narrowingByTypeofInSwitch.ts, 10, 1))
748+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
749+
750+
case 'undefined': assertUndefined(x); return;
751+
>assertUndefined : Symbol(assertUndefined, Decl(narrowingByTypeofInSwitch.ts, 30, 1))
752+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
753+
754+
case 'number': assertNever(x); return;
755+
>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0))
756+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
757+
758+
default: const _y: {} = assertNever(x); return;
759+
>_y : Symbol(_y, Decl(narrowingByTypeofInSwitch.ts, 261, 22))
760+
>assertNever : Symbol(assertNever, Decl(narrowingByTypeofInSwitch.ts, 0, 0))
761+
>x : Symbol(x, Decl(narrowingByTypeofInSwitch.ts, 251, 27))
762+
}
763+
}
764+

0 commit comments

Comments
 (0)