From c48cd4a93be6a7f993f8d3ff5010537ca52587ad Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 18 Jul 2016 13:16:11 -0700 Subject: [PATCH] Unify 'boolean' and 'true | false' --- src/compiler/checker.ts | 155 ++++++++++++++++++++++++++-------------- src/compiler/types.ts | 3 +- 2 files changed, 102 insertions(+), 56 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bbe74231f5a..c7983d634a5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -108,15 +108,21 @@ namespace ts { isOptionalParameter }; + const tupleTypes: Map = {}; + const unionTypes: Map = {}; + const intersectionTypes: Map = {}; + const stringLiteralTypes: Map = {}; + const numericLiteralTypes: Map = {}; + const unknownSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, "unknown"); const resolvingSymbol = createSymbol(SymbolFlags.Transient, "__resolving__"); const anyType = createIntrinsicType(TypeFlags.Any, "any"); const stringType = createIntrinsicType(TypeFlags.String, "string"); const numberType = createIntrinsicType(TypeFlags.Number, "number"); - const booleanType = createIntrinsicType(TypeFlags.Boolean, "boolean"); const trueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true"); const falseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false"); + const booleanType = createBooleanType([trueType, falseType]); const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol"); const voidType = createIntrinsicType(TypeFlags.Void, "void"); const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined"); @@ -196,14 +202,8 @@ namespace ts { let flowLoopCount = 0; let visitedFlowCount = 0; - const tupleTypes: Map = {}; - const unionTypes: Map = {}; - const intersectionTypes: Map = {}; - const stringLiteralTypes: Map = {}; - const numericLiteralTypes: Map = {}; const emptyStringType = getLiteralTypeForText(TypeFlags.StringLiteral, ""); const zeroType = getLiteralTypeForText(TypeFlags.NumberLiteral, "0"); - const trueFalseType = getUnionType([trueType, falseType]); const resolutionTargets: TypeSystemEntity[] = []; const resolutionResults: boolean[] = []; @@ -1547,6 +1547,13 @@ namespace ts { return type; } + function createBooleanType(trueFalseTypes: Type[]): IntrinsicType { + const type = getUnionType(trueFalseTypes, /*noSubtypeReduction*/ true); + type.flags |= TypeFlags.Boolean; + type.intrinsicName = "boolean"; + return type; + } + function createObjectType(kind: TypeFlags, symbol?: Symbol): ObjectType { const type = createType(kind); type.symbol = symbol; @@ -1921,6 +1928,19 @@ namespace ts { return result; } + function replaceTrueFalseWithBoolean(types: Type[]): Type[] { + if (contains(types, trueType) && contains(types, falseType)) { + const result: Type[] = []; + for (const t of types) { + if (t !== falseType) { + result.push(t === trueType ? booleanType : t); + } + } + return result; + } + return types; + } + function visibilityToString(flags: NodeFlags) { if (flags === NodeFlags.Private) { return "private"; @@ -2213,7 +2233,12 @@ namespace ts { if (flags & TypeFormatFlags.InElementType) { writePunctuation(writer, SyntaxKind.OpenParenToken); } - writeTypeList(type.types, type.flags & TypeFlags.Union ? SyntaxKind.BarToken : SyntaxKind.AmpersandToken); + if (type.flags & TypeFlags.Union) { + writeTypeList(replaceTrueFalseWithBoolean(type.types), SyntaxKind.BarToken); + } + else { + writeTypeList(type.types, SyntaxKind.AmpersandToken); + } if (flags & TypeFormatFlags.InElementType) { writePunctuation(writer, SyntaxKind.CloseParenToken); } @@ -5661,7 +5686,7 @@ namespace ts { if (type.flags & TypeFlags.Tuple) { return createTupleType(instantiateList((type).elementTypes, mapper, instantiateType)); } - if (type.flags & TypeFlags.Union) { + if (type.flags & TypeFlags.Union && !(type.flags & TypeFlags.Primitive)) { return getUnionType(instantiateList((type).types, mapper, instantiateType), /*noSubtypeReduction*/ true); } if (type.flags & TypeFlags.Intersection) { @@ -6070,10 +6095,10 @@ namespace ts { // Note that these checks are specifically ordered to produce correct results. if (source.flags & TypeFlags.Union) { if (relation === comparableRelation) { - result = someTypeRelatedToType(source as UnionType, target, reportErrors); + result = someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)); } else { - result = eachTypeRelatedToType(source as UnionType, target, reportErrors); + result = eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)); } if (result) { @@ -6110,12 +6135,9 @@ namespace ts { } } if (target.flags & TypeFlags.Union) { - if (result = typeRelatedToSomeType(source, target, reportErrors && !(source.flags & TypeFlags.Primitive))) { + if (result = typeRelatedToSomeType(source, target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive))) { return result; } - if (source === booleanType && contains((target).types, trueType) && contains((target).types, falseType)) { - return Ternary.True; - } } } @@ -6969,7 +6991,9 @@ namespace ts { } function isUnitUnionType(type: Type): boolean { - return type.flags & TypeFlags.Union ? !forEach((type).types, t => !isUnitType(t)) : isUnitType(type); + return type.flags & TypeFlags.Boolean ? true : + type.flags & TypeFlags.Union ? !forEach((type).types, t => !isUnitType(t)) : + isUnitType(type); } function getBaseTypeOfUnitType(type: Type): Type { @@ -6981,10 +7005,6 @@ namespace ts { type; } - function isUnionWithTrueOrFalse(type: Type) { - return type.flags & TypeFlags.Union && (contains((type).types, trueType) || contains((type).types, falseType)); - } - /** * Check if a Type was written as a tuple type literal. * Prefer using isTupleLikeType() unless the use of `elementTypes` is required. @@ -7001,12 +7021,15 @@ namespace ts { return result; } + // Returns the String, Number, Boolean, StringLiteral, NumberLiteral, BooleanLiteral, Void, Undefined, or Null + // flags for the string, number, boolean, "", 0, false, void, undefined, or null types respectively. Returns + // no flags for all other types (including non-falsy literal types). function getFalsyFlags(type: Type): TypeFlags { - return type === emptyStringType ? TypeFlags.StringLiteral : - type === zeroType ? TypeFlags.NumberLiteral : - type === falseType ? TypeFlags.BooleanLiteral : - type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((type).types) : - type.flags & TypeFlags.AlwaysPossiblyFalsy; + return type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((type).types) : + type.flags & TypeFlags.StringLiteral ? type === emptyStringType ? TypeFlags.StringLiteral : 0 : + type.flags & TypeFlags.NumberLiteral ? type === zeroType ? TypeFlags.NumberLiteral : 0 : + type.flags & TypeFlags.BooleanLiteral ? type === falseType ? TypeFlags.BooleanLiteral : 0 : + type.flags & TypeFlags.PossiblyFalsy; } function includeFalsyTypes(type: Type, flags: TypeFlags) { @@ -10403,8 +10426,11 @@ namespace ts { checkClassPropertyAccess(node, left, apparentType, prop); } - const propType = prop.flags & SymbolFlags.EnumMember && getParentOfSymbol(prop).flags & SymbolFlags.ConstEnum && - isLiteralTypeContext(node) ? getDeclaredTypeOfSymbol(prop) : getTypeOfSymbol(prop); + let propType = getTypeOfSymbol(prop); + if (prop.flags & SymbolFlags.EnumMember && getParentOfSymbol(prop).flags & SymbolFlags.ConstEnum && isLiteralContextForType(node, propType)) { + propType = getDeclaredTypeOfSymbol(prop); + } + // Only compute control flow type if this is a property access expression that isn't an // assignment target, and the referenced property was declared as a variable, property, // accessor, or optional method. @@ -12460,7 +12486,7 @@ namespace ts { function checkPrefixUnaryExpression(node: PrefixUnaryExpression): Type { const operandType = checkExpression(node.operand); - if (node.operator === SyntaxKind.MinusToken && node.operand.kind === SyntaxKind.NumericLiteral && isLiteralTypeContext(node)) { + if (node.operator === SyntaxKind.MinusToken && node.operand.kind === SyntaxKind.NumericLiteral && isLiteralContextForType(node, numberType)) { return getLiteralTypeForText(TypeFlags.NumberLiteral, "" + -(node.operand).text); } switch (node.operator) { @@ -12475,7 +12501,6 @@ namespace ts { const facts = getTypeFacts(operandType) & (TypeFacts.Truthy | TypeFacts.Falsy); return facts === TypeFacts.Truthy ? falseType : facts === TypeFacts.Falsy ? trueType : - isUnionWithTrueOrFalse(operandType) ? trueFalseType : booleanType; case SyntaxKind.PlusPlusToken: case SyntaxKind.MinusMinusToken: @@ -12866,7 +12891,7 @@ namespace ts { return checkInExpression(left, right, leftType, rightType); case SyntaxKind.AmpersandAmpersandToken: return getTypeFacts(leftType) & TypeFacts.Truthy ? - strictNullChecks ? includeFalsyTypes(rightType, getFalsyFlags(leftType)) : rightType : + includeFalsyTypes(rightType, getFalsyFlags(strictNullChecks ? leftType : getBaseTypeOfUnitType(rightType))) : leftType; case SyntaxKind.BarBarToken: return getTypeFacts(leftType) & TypeFacts.Falsy ? @@ -13000,45 +13025,64 @@ namespace ts { return getUnionType([type1, type2]); } - function isLiteralUnionType(type: Type): boolean { - return type.flags & TypeFlags.Literal ? true : - type.flags & TypeFlags.Enum ? (type.symbol.flags & SymbolFlags.EnumMember) !== 0 : - type.flags & TypeFlags.Union ? forEach((type).types, isLiteralUnionType) : - false; - } - - function hasLiteralContextualType(node: Expression) { - const contextualType = getContextualType(node); - if (!contextualType) { - return false; - } - if (contextualType.flags & TypeFlags.TypeParameter) { - const apparentType = getApparentTypeOfTypeParameter(contextualType); - if (apparentType.flags & (TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.BooleanLike)) { - return true; + function typeContainsEnumLiteral(type: Type, enumType: Type) { + if (type.flags & TypeFlags.Union) { + for (const t of (type).types) { + if (t.flags & TypeFlags.Enum && t.symbol.flags & SymbolFlags.EnumMember && t.symbol.parent === enumType.symbol) { + return true; + } } } - return isLiteralUnionType(contextualType); + if (type.flags & TypeFlags.Enum) { + return type.symbol.flags & SymbolFlags.EnumMember && type.symbol.parent === enumType.symbol; + } + return false; } - function isLiteralTypeContext(node: Expression) { - return isLiteralTypeLocation(node) || hasLiteralContextualType(node); + function isLiteralContextForType(node: Expression, type: Type) { + if (isLiteralTypeLocation(node)) { + return true; + } + let contextualType = getContextualType(node); + if (contextualType) { + if (contextualType.flags & TypeFlags.TypeParameter) { + const apparentType = getApparentTypeOfTypeParameter(contextualType); + // If the type parameter is constrained to the base primitive type we're checking for, + // consider this a literal context. For example, given a type parameter 'T extends string', + // this causes us to infer string literal types for T. + if (type === apparentType) { + return true; + } + contextualType = apparentType; + } + if (type.flags & TypeFlags.String) { + return maybeTypeOfKind(contextualType, TypeFlags.StringLiteral); + } + if (type.flags & TypeFlags.Number) { + return maybeTypeOfKind(contextualType, TypeFlags.NumberLiteral); + } + if (type.flags & TypeFlags.Boolean) { + return maybeTypeOfKind(contextualType, TypeFlags.BooleanLiteral) && !isTypeAssignableTo(booleanType, contextualType); + } + if (type.flags & TypeFlags.Enum && type.symbol.flags & SymbolFlags.ConstEnum) { + return typeContainsEnumLiteral(contextualType, type); + } + } + return false; } function checkLiteralExpression(node: Expression): Type { if (node.kind === SyntaxKind.NumericLiteral) { checkGrammarNumericLiteral(node); } - const hasLiteralType = isLiteralTypeContext(node); switch (node.kind) { case SyntaxKind.StringLiteral: - return hasLiteralType ? getLiteralTypeForText(TypeFlags.StringLiteral, (node).text) : stringType; + return isLiteralContextForType(node, stringType) ? getLiteralTypeForText(TypeFlags.StringLiteral, (node).text) : stringType; case SyntaxKind.NumericLiteral: - return hasLiteralType ? getLiteralTypeForText(TypeFlags.NumberLiteral, (node).text) : numberType; + return isLiteralContextForType(node, numberType) ? getLiteralTypeForText(TypeFlags.NumberLiteral, (node).text) : numberType; case SyntaxKind.TrueKeyword: - return hasLiteralType ? trueType : booleanType; case SyntaxKind.FalseKeyword: - return hasLiteralType ? falseType : booleanType; + return isLiteralContextForType(node, booleanType) ? node.kind === SyntaxKind.TrueKeyword ? trueType : falseType : booleanType; } } @@ -18324,6 +18368,9 @@ namespace ts { } } + // The built-in boolean type is 'true | false', also mark 'false | true' as a boolean type + createBooleanType([falseType, trueType]); + // Setup global builtins addToSymbolTable(globals, builtinGlobals, Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8c1f3f0f9ed..3d1962826d1 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2254,7 +2254,6 @@ namespace ts { /* @internal */ DefinitelyFalsy = StringLiteral | NumberLiteral | BooleanLiteral | Void | Undefined | Null, PossiblyFalsy = DefinitelyFalsy | String | Number | Boolean, - AlwaysPossiblyFalsy = String | Number | Boolean | Void | Undefined | Null, /* @internal */ Intrinsic = Any | String | Number | Boolean | BooleanLiteral | ESSymbol | Void | Undefined | Null | Never, /* @internal */ @@ -2269,7 +2268,7 @@ namespace ts { // 'Narrowable' types are types where narrowing actually narrows. // This *should* be every type other than null, undefined, void, and never Narrowable = Any | StructuredType | TypeParameter | StringLike | NumberLike | BooleanLike | ESSymbol, - NotUnionOrUnit = Any | String | Number | Boolean | ESSymbol | ObjectType, + NotUnionOrUnit = Any | String | Number | ESSymbol | ObjectType, /* @internal */ RequiresWidening = ContainsWideningType | ContainsObjectLiteral, /* @internal */