Skip to content

Commit 5c6a856

Browse files
committed
CFA for dependent variables destructured from discriminated union
1 parent 8c27075 commit 5c6a856

File tree

2 files changed

+107
-37
lines changed

2 files changed

+107
-37
lines changed

src/compiler/checker.ts

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8466,12 +8466,16 @@ namespace ts {
84668466

84678467
/** Return the inferred type for a binding element */
84688468
function getTypeForBindingElement(declaration: BindingElement): Type | undefined {
8469-
const pattern = declaration.parent;
8470-
let parentType = getTypeForBindingElementParent(pattern.parent);
8471-
// If no type or an any type was inferred for parent, infer that for the binding element
8472-
if (!parentType || isTypeAny(parentType)) {
8469+
const parentType = getTypeForBindingElementParent(declaration.parent.parent);
8470+
return parentType && getBindingElementTypeFromParentType(declaration, parentType);
8471+
}
8472+
8473+
function getBindingElementTypeFromParentType(declaration: BindingElement, parentType: Type): Type {
8474+
// If an any type was inferred for parent, infer that for the binding element
8475+
if (isTypeAny(parentType)) {
84738476
return parentType;
84748477
}
8478+
const pattern = declaration.parent;
84758479
// Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation
84768480
if (strictNullChecks && declaration.flags & NodeFlags.Ambient && isParameterDeclaration(declaration)) {
84778481
parentType = getNonNullableType(parentType);
@@ -22534,30 +22538,6 @@ namespace ts {
2253422538
return false;
2253522539
}
2253622540

22537-
function getPropertyAccess(expr: Expression) {
22538-
if (isAccessExpression(expr)) {
22539-
return expr;
22540-
}
22541-
if (isIdentifier(expr)) {
22542-
const symbol = getResolvedSymbol(expr);
22543-
if (isConstVariable(symbol)) {
22544-
const declaration = symbol.valueDeclaration!;
22545-
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
22546-
if (isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer)) {
22547-
return declaration.initializer;
22548-
}
22549-
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
22550-
if (isBindingElement(declaration) && !declaration.initializer) {
22551-
const parent = declaration.parent.parent;
22552-
if (isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer))) {
22553-
return declaration;
22554-
}
22555-
}
22556-
}
22557-
}
22558-
return undefined;
22559-
}
22560-
2256122541
function getAccessedPropertyName(access: AccessExpression | BindingElement): __String | undefined {
2256222542
let propertyName;
2256322543
return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText :
@@ -23526,19 +23506,19 @@ namespace ts {
2352623506
return false;
2352723507
}
2352823508

23529-
function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType = declaredType, flowContainer?: Node) {
23509+
function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType = declaredType, flowContainer?: Node, flowNode = reference.flowNode) {
2353023510
let key: string | undefined;
2353123511
let isKeySet = false;
2353223512
let flowDepth = 0;
2353323513
if (flowAnalysisDisabled) {
2353423514
return errorType;
2353523515
}
23536-
if (!reference.flowNode) {
23516+
if (!flowNode) {
2353723517
return declaredType;
2353823518
}
2353923519
flowInvocationCount++;
2354023520
const sharedFlowStart = sharedFlowCount;
23541-
const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode));
23521+
const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(flowNode));
2354223522
sharedFlowCount = sharedFlowStart;
2354323523
// When the reference is 'x' in an 'x.length', 'x.push(value)', 'x.unshift(value)' or x[n] = value' operation,
2354423524
// we give type 'any[]' to 'x' instead of using the type determined by control flow analysis such that operations
@@ -23982,13 +23962,58 @@ namespace ts {
2398223962
return result;
2398323963
}
2398423964

23965+
function getCandidateDiscriminantPropertyAccess(expr: Expression) {
23966+
if (isBindingPattern(reference)) {
23967+
// When the reference is a binding pattern, we are narrowing a pesudo-reference in getNarrowedTypeOfSymbol.
23968+
// An identifier for a destructuring variable declared in the same binding pattern is a candidate.
23969+
if (isIdentifier(expr)) {
23970+
const symbol = getResolvedSymbol(expr);
23971+
const declaration = symbol.valueDeclaration;
23972+
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && reference === declaration.parent) {
23973+
return declaration;
23974+
}
23975+
}
23976+
}
23977+
else if (isAccessExpression(expr)) {
23978+
// An access expression is a candidate if the reference matches the left hand expression.
23979+
if (isMatchingReference(reference, expr.expression)) {
23980+
return expr;
23981+
}
23982+
}
23983+
else if (isIdentifier(expr)) {
23984+
const symbol = getResolvedSymbol(expr);
23985+
if (isConstVariable(symbol)) {
23986+
const declaration = symbol.valueDeclaration!;
23987+
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
23988+
if (isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer) &&
23989+
isMatchingReference(reference, declaration.initializer.expression)) {
23990+
return declaration.initializer;
23991+
}
23992+
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
23993+
if (isBindingElement(declaration) && !declaration.initializer) {
23994+
const parent = declaration.parent.parent;
23995+
if (isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer)) &&
23996+
isMatchingReference(reference, parent.initializer)) {
23997+
return declaration;
23998+
}
23999+
}
24000+
}
24001+
}
24002+
return undefined;
24003+
}
24004+
2398524005
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
23986-
let access, name;
2398724006
const type = declaredType.flags & TypeFlags.Union ? declaredType : computedType;
23988-
return type.flags & TypeFlags.Union && (access = getPropertyAccess(expr)) && (name = getAccessedPropertyName(access)) &&
23989-
isMatchingReference(reference, isAccessExpression(access) ? access.expression : access.parent.parent.initializer!) &&
23990-
isDiscriminantProperty(type, name) ?
23991-
access : undefined;
24007+
if (type.flags & TypeFlags.Union) {
24008+
const access = getCandidateDiscriminantPropertyAccess(expr);
24009+
if (access) {
24010+
const name = getAccessedPropertyName(access);
24011+
if (name && isDiscriminantProperty(type, name)) {
24012+
return access;
24013+
}
24014+
}
24015+
}
24016+
return undefined;
2399224017
}
2399324018

2399424019
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement, narrowType: (t: Type) => Type): Type {
@@ -24801,6 +24826,50 @@ namespace ts {
2480124826
}
2480224827
}
2480324828

24829+
function getNarrowedTypeOfSymbol(symbol: Symbol, location: Identifier) {
24830+
// If we have a non-rest binding element with no initializer declared as a const variable or a const-like
24831+
// parameter (a parameter for which there are no assignments in the function body), and if the parent type
24832+
// for the destructuring is a union type, one or more of the binding elements may represent discriminant
24833+
// properties, and we want the effects of conditional checks on such discriminants to affect the types of
24834+
// other binding elements from the same destructuring. Consider:
24835+
//
24836+
// type Action =
24837+
// | { kind: 'A', payload: number }
24838+
// | { kind: 'B', payload: string };
24839+
//
24840+
// function f1({ kind, payload }: Action) {
24841+
// if (kind === 'A') {
24842+
// payload.toFixed();
24843+
// }
24844+
// if (kind === 'B') {
24845+
// payload.toUpperCase();
24846+
// }
24847+
// }
24848+
//
24849+
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
24850+
// the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference
24851+
// as if it occurred in the specified location. We then recompute the narrowed binding element type by
24852+
// destructuring from the narrowed parent type.
24853+
const declaration = symbol.valueDeclaration;
24854+
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken) {
24855+
const parent = declaration.parent.parent;
24856+
if (parent.kind === SyntaxKind.VariableDeclaration && getCombinedNodeFlags(declaration) && NodeFlags.Const || parent.kind === SyntaxKind.Parameter && !isSymbolAssigned(symbol)) {
24857+
const links = getNodeLinks(location);
24858+
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
24859+
links.flags |= NodeCheckFlags.InCheckIdentifier;
24860+
const parentType = getTypeForBindingElementParent(parent);
24861+
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
24862+
if (parentType && parentType.flags & TypeFlags.Union) {
24863+
const pattern = declaration.parent;
24864+
const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode);
24865+
return getBindingElementTypeFromParentType(declaration, narrowedType);
24866+
}
24867+
}
24868+
}
24869+
}
24870+
return getTypeOfSymbol(symbol);
24871+
}
24872+
2480424873
function checkIdentifier(node: Identifier, checkMode: CheckMode | undefined): Type {
2480524874
const symbol = getResolvedSymbol(node);
2480624875
if (symbol === unknownSymbol) {
@@ -24884,7 +24953,7 @@ namespace ts {
2488424953

2488524954
checkNestedBlockScopedBinding(node, symbol);
2488624955

24887-
let type = getTypeOfSymbol(localOrExportSymbol);
24956+
let type = getNarrowedTypeOfSymbol(localOrExportSymbol, node);
2488824957
const assignmentKind = getAssignmentTargetKind(node);
2488924958

2489024959
if (assignmentKind) {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5073,6 +5073,7 @@ namespace ts {
50735073
ConstructorReferenceInClass = 0x02000000, // Binding to a class constructor inside of the class's body.
50745074
ContainsClassWithPrivateIdentifiers = 0x04000000, // Marked on all block-scoped containers containing a class with private identifiers.
50755075
ContainsSuperPropertyInStaticInitializer = 0x08000000, // Marked on all block-scoped containers containing a static initializer with 'super.x' or 'super[x]'.
5076+
InCheckIdentifier = 0x10000000,
50765077
}
50775078

50785079
/* @internal */

0 commit comments

Comments
 (0)