Skip to content

Commit

Permalink
Merge pull request #8548 from Microsoft/typeGuardAsAssertion
Browse files Browse the repository at this point in the history
Type guards as assertions
  • Loading branch information
ahejlsberg committed May 11, 2016
2 parents b68e939 + 9f30d9f commit 89506c1
Show file tree
Hide file tree
Showing 11 changed files with 1,099 additions and 44 deletions.
19 changes: 1 addition & 18 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,23 +696,6 @@ namespace ts {
};
}

function skipSimpleConditionalFlow(flow: FlowNode) {
// We skip over simple conditional flows of the form 'x ? aaa : bbb', where 'aaa' and 'bbb' contain
// no constructs that affect control flow type analysis. Such simple flows have no effect on the
// code paths that follow and ignoring them means we'll do less work.
if (flow.flags & FlowFlags.BranchLabel && (<FlowLabel>flow).antecedents.length === 2) {
const a = (<FlowLabel>flow).antecedents[0];
const b = (<FlowLabel>flow).antecedents[1];
if ((a.flags & FlowFlags.TrueCondition && b.flags & FlowFlags.FalseCondition ||
a.flags & FlowFlags.FalseCondition && b.flags & FlowFlags.TrueCondition) &&
(<FlowCondition>a).antecedent === (<FlowCondition>b).antecedent &&
(<FlowCondition>a).expression === (<FlowCondition>b).expression) {
return (<FlowCondition>a).antecedent;
}
}
return flow;
}

function finishFlowLabel(flow: FlowLabel): FlowNode {
const antecedents = flow.antecedents;
if (!antecedents) {
Expand All @@ -721,7 +704,7 @@ namespace ts {
if (antecedents.length === 1) {
return antecedents[0];
}
return skipSimpleConditionalFlow(flow);
return flow;
}

function isStatementCondition(node: Node) {
Expand Down
47 changes: 34 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ namespace ts {
const unknownType = createIntrinsicType(TypeFlags.Any, "unknown");

const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
const emptyUnionType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
const nothingType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
const emptyGenericType = <GenericType><ObjectType>createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
emptyGenericType.instantiations = {};

Expand Down Expand Up @@ -2030,7 +2030,7 @@ namespace ts {
writeUnionOrIntersectionType(<UnionOrIntersectionType>type, flags);
}
else if (type.flags & TypeFlags.Anonymous) {
if (type === emptyUnionType) {
if (type === nothingType) {
writer.writeKeyword("nothing");
}
else {
Expand Down Expand Up @@ -5006,7 +5006,7 @@ namespace ts {
if (type.flags & TypeFlags.Undefined) typeSet.containsUndefined = true;
if (type.flags & TypeFlags.Null) typeSet.containsNull = true;
}
else if (type !== emptyUnionType && !contains(typeSet, type)) {
else if (type !== nothingType && !contains(typeSet, type)) {
typeSet.push(type);
}
}
Expand Down Expand Up @@ -5047,7 +5047,10 @@ namespace ts {
// a named type that circularly references itself.
function getUnionType(types: Type[], noSubtypeReduction?: boolean): Type {
if (types.length === 0) {
return emptyUnionType;
return nothingType;
}
if (types.length === 1) {
return types[0];
}
const typeSet = [] as TypeSet;
addTypesToSet(typeSet, types, TypeFlags.Union);
Expand All @@ -5064,7 +5067,7 @@ namespace ts {
if (typeSet.length === 0) {
return typeSet.containsNull ? nullType :
typeSet.containsUndefined ? undefinedType :
emptyUnionType;
nothingType;
}
else if (typeSet.length === 1) {
return typeSet[0];
Expand Down Expand Up @@ -7483,7 +7486,7 @@ namespace ts {

function getTypeWithFacts(type: Type, include: TypeFacts) {
if (!(type.flags & TypeFlags.Union)) {
return getTypeFacts(type) & include ? type : emptyUnionType;
return getTypeFacts(type) & include ? type : nothingType;
}
let firstType: Type;
let types: Type[];
Expand All @@ -7500,7 +7503,7 @@ namespace ts {
}
}
}
return firstType ? types ? getUnionType(types, /*noSubtypeReduction*/ true) : firstType : emptyUnionType;
return firstType ? types ? getUnionType(types, /*noSubtypeReduction*/ true) : firstType : nothingType;
}

function getTypeWithDefault(type: Type, defaultExpression: Expression) {
Expand Down Expand Up @@ -7618,6 +7621,9 @@ namespace ts {
const visitedFlowStart = visitedFlowCount;
const result = getTypeAtFlowNode(reference.flowNode);
visitedFlowCount = visitedFlowStart;
if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull) === nothingType) {
return declaredType;
}
return result;

function getTypeAtFlowNode(flow: FlowNode): Type {
Expand Down Expand Up @@ -7702,7 +7708,22 @@ namespace ts {
}

function getTypeAtFlowCondition(flow: FlowCondition) {
return narrowType(getTypeAtFlowNode(flow.antecedent), flow.expression, (flow.flags & FlowFlags.TrueCondition) !== 0);
let type = getTypeAtFlowNode(flow.antecedent);
if (type !== nothingType) {
// If we have an antecedent type (meaning we're reachable in some way), we first
// attempt to narrow the antecedent type. If that produces the nothing type, then
// we take the type guard as an indication that control could reach here in a
// manner not understood by the control flow analyzer (e.g. a function argument
// has an invalid type, or a nested function has possibly made an assignment to a
// captured variable). We proceed by reverting to the declared type and then
// narrow that.
const assumeTrue = (flow.flags & FlowFlags.TrueCondition) !== 0;
type = narrowType(type, flow.expression, assumeTrue);
if (type === nothingType) {
type = narrowType(declaredType, flow.expression, assumeTrue);
}
}
return type;
}

function getTypeAtFlowBranchLabel(flow: FlowLabel) {
Expand All @@ -7720,7 +7741,7 @@ namespace ts {
antecedentTypes.push(type);
}
}
return antecedentTypes.length === 1 ? antecedentTypes[0] : getUnionType(antecedentTypes);
return getUnionType(antecedentTypes);
}

function getTypeAtFlowLoopLabel(flow: FlowLabel) {
Expand Down Expand Up @@ -7770,7 +7791,7 @@ namespace ts {
antecedentTypes.push(type);
}
}
return cache[key] = antecedentTypes.length === 1 ? antecedentTypes[0] : getUnionType(antecedentTypes);
return cache[key] = getUnionType(antecedentTypes);
}

function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type {
Expand Down Expand Up @@ -7921,7 +7942,7 @@ namespace ts {
const targetType = type.flags & TypeFlags.TypeParameter ? getApparentType(type) : type;
return isTypeAssignableTo(candidate, targetType) ? candidate :
isTypeAssignableTo(type, candidate) ? type :
emptyUnionType;
nothingType;
}

function narrowTypeByTypePredicate(type: Type, callExpression: CallExpression, assumeTrue: boolean): Type {
Expand Down Expand Up @@ -14726,7 +14747,7 @@ namespace ts {
arrayType = getUnionType(filter((arrayOrStringType as UnionType).types, t => !(t.flags & TypeFlags.StringLike)));
}
else if (arrayOrStringType.flags & TypeFlags.StringLike) {
arrayType = emptyUnionType;
arrayType = nothingType;
}
const hasStringConstituent = arrayOrStringType !== arrayType;
let reportedError = false;
Expand All @@ -14738,7 +14759,7 @@ namespace ts {

// Now that we've removed all the StringLike types, if no constituents remain, then the entire
// arrayOrStringType was a string.
if (arrayType === emptyUnionType) {
if (arrayType === nothingType) {
return stringType;
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/controlFlowBinaryOrExpression.types
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ if (isNodeList(sourceObj)) {
if (isHTMLCollection(sourceObj)) {
>isHTMLCollection(sourceObj) : boolean
>isHTMLCollection : (sourceObj: any) => sourceObj is HTMLCollection
>sourceObj : { a: string; } | HTMLCollection
>sourceObj : HTMLCollection | { a: string; }

sourceObj.length;
>sourceObj.length : number
Expand All @@ -99,7 +99,7 @@ if (isNodeList(sourceObj) || isHTMLCollection(sourceObj)) {
>isNodeList(sourceObj) || isHTMLCollection(sourceObj) : boolean
>isNodeList(sourceObj) : boolean
>isNodeList : (sourceObj: any) => sourceObj is NodeList
>sourceObj : { a: string; } | HTMLCollection
>sourceObj : HTMLCollection | { a: string; }
>isHTMLCollection(sourceObj) : boolean
>isHTMLCollection : (sourceObj: any) => sourceObj is HTMLCollection
>sourceObj : { a: string; }
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/typeGuardEnums.types
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ else {
if (typeof x !== "number") {
>typeof x !== "number" : boolean
>typeof x : string
>x : number | string | E | V
>x : number | string
>"number" : string

x; // string
>x : string
}
else {
x; // number|E|V
>x : number | E | V
>x : number
}

8 changes: 4 additions & 4 deletions tests/baselines/reference/typeGuardNesting.types
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'stri
>(typeof strOrBool === 'boolean') : boolean
>typeof strOrBool === 'boolean' : boolean
>typeof strOrBool : string
>strOrBool : boolean | string
>strOrBool : string | boolean
>'boolean' : string
>strOrBool : boolean
>false : boolean
Expand All @@ -56,7 +56,7 @@ if ((typeof strOrBool === 'boolean' && !strOrBool) || typeof strOrBool === 'stri
>(typeof strOrBool !== 'string') : boolean
>typeof strOrBool !== 'string' : boolean
>typeof strOrBool : string
>strOrBool : boolean | string
>strOrBool : string | boolean
>'string' : string
>strOrBool : boolean
>false : boolean
Expand Down Expand Up @@ -94,7 +94,7 @@ if ((typeof strOrBool !== 'string' && !strOrBool) || typeof strOrBool !== 'boole
>(typeof strOrBool === 'boolean') : boolean
>typeof strOrBool === 'boolean' : boolean
>typeof strOrBool : string
>strOrBool : boolean | string
>strOrBool : string | boolean
>'boolean' : string
>strOrBool : boolean
>false : boolean
Expand All @@ -116,7 +116,7 @@ if ((typeof strOrBool !== 'string' && !strOrBool) || typeof strOrBool !== 'boole
>(typeof strOrBool !== 'string') : boolean
>typeof strOrBool !== 'string' : boolean
>typeof strOrBool : string
>strOrBool : boolean | string
>strOrBool : string | boolean
>'string' : string
>strOrBool : boolean
>false : boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if (typeof stringOrNumber === "number") {
>"number" : string

stringOrNumber;
>stringOrNumber : nothing
>stringOrNumber : string
}
}

Expand All @@ -31,6 +31,6 @@ if (typeof stringOrNumber === "number" && typeof stringOrNumber !== "number") {
>"number" : string

stringOrNumber;
>stringOrNumber : nothing
>stringOrNumber : string
}

6 changes: 3 additions & 3 deletions tests/baselines/reference/typeGuardTypeOfUndefined.types
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function test2(a: any) {
>"boolean" : string

a;
>a : nothing
>a : boolean
}
else {
a;
Expand Down Expand Up @@ -129,7 +129,7 @@ function test5(a: boolean | void) {
}
else {
a;
>a : nothing
>a : void
}
}
else {
Expand Down Expand Up @@ -188,7 +188,7 @@ function test7(a: boolean | void) {
}
else {
a;
>a : nothing
>a : void
}
}

Expand Down
Loading

0 comments on commit 89506c1

Please sign in to comment.