Skip to content

Narrow by property assignment in an object literal #22006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
42 changes: 35 additions & 7 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,11 @@ namespace ts {
return { flags: FlowFlags.Assignment, antecedent, node };
}

function createFlowInitializer(antecedent: FlowNode, node: Expression | VariableDeclaration | BindingElement): FlowNode {
setFlowNodeReferenced(antecedent);
return { flags: FlowFlags.Initializer, antecedent, node };
}

function createFlowArrayMutation(antecedent: FlowNode, node: CallExpression | BinaryExpression): FlowNode {
setFlowNodeReferenced(antecedent);
const res: FlowArrayMutation = { flags: FlowFlags.ArrayMutation, antecedent, node };
Expand Down Expand Up @@ -1234,12 +1239,28 @@ namespace ts {
}
}

function bindInitializerFlow(node: Expression): void {
if (isNarrowableReference(node)) {
currentFlow = createFlowInitializer(currentFlow, node);
}
else if (isObjectLiteralExpression(node)) {
for (const p of node.properties) {
if (p.name && p.name.kind === SyntaxKind.Identifier) {
bindInitializerFlow(p.name);
}
if (p.kind === SyntaxKind.PropertyAssignment) {
bindInitializerFlow(p.initializer);
}
}
}
}

function bindAssignmentTargetFlow(node: Expression) {
if (isNarrowableReference(node)) {
currentFlow = createFlowAssignment(currentFlow, node);
}
else if (node.kind === SyntaxKind.ArrayLiteralExpression) {
for (const e of (<ArrayLiteralExpression>node).elements) {
else if (isArrayLiteralExpression(node)) {
for (const e of node.elements) {
if (e.kind === SyntaxKind.SpreadElement) {
bindAssignmentTargetFlow((<SpreadElement>e).expression);
}
Expand All @@ -1248,16 +1269,16 @@ namespace ts {
}
}
}
else if (node.kind === SyntaxKind.ObjectLiteralExpression) {
for (const p of (<ObjectLiteralExpression>node).properties) {
else if (isObjectLiteralExpression(node)) {
for (const p of node.properties) {
if (p.kind === SyntaxKind.PropertyAssignment) {
bindDestructuringTargetFlow((<PropertyAssignment>p).initializer);
bindDestructuringTargetFlow(p.initializer);
}
else if (p.kind === SyntaxKind.ShorthandPropertyAssignment) {
bindAssignmentTargetFlow((<ShorthandPropertyAssignment>p).name);
bindAssignmentTargetFlow(p.name);
}
else if (p.kind === SyntaxKind.SpreadAssignment) {
bindAssignmentTargetFlow((<SpreadAssignment>p).expression);
bindAssignmentTargetFlow(p.expression);
}
}
}
Expand Down Expand Up @@ -1358,6 +1379,13 @@ namespace ts {
}
else {
currentFlow = createFlowAssignment(currentFlow, node);
if (isVariableDeclaration(node) &&
node.type &&
node.initializer &&
isIdentifier(node.name) &&
isObjectLiteralExpression(node.initializer)) {
bindInitializerFlow(node.initializer);
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12630,6 +12630,13 @@ namespace ts {
continue;
}
}
else if (flags & FlowFlags.Initializer) {
type = getTypeAtFlowInitializer(flow as FlowInitializer);
if (!type) {
flow = (<FlowInitializer>flow).antecedent;
continue;
}
}
else if (flags & FlowFlags.Condition) {
type = getTypeAtFlowCondition(<FlowCondition>flow);
}
Expand Down Expand Up @@ -12678,6 +12685,30 @@ namespace ts {
}
}

function getTypeAtFlowInitializer(flow: FlowInitializer): Type {
const node = flow.node;
if (isMatchingInitializerReference(reference, node.parent)) {
if (declaredType.flags & TypeFlags.Union) {
const sourceNode = isPropertyAssignment(node.parent) ? node.parent.initializer : (node.parent as ShorthandPropertyAssignment).name;
return getAssignmentReducedType(declaredType as UnionType, getTypeOfNode(sourceNode));
}
return declaredType;
}
return undefined;
}

function isMatchingInitializerReference(reference: Node, initializer: Node): boolean {
if (isIdentifier(reference)) {
return isVariableDeclaration(initializer) && getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(reference)) === getSymbolOfNode(initializer);
}
else if (isPropertyAccessExpression(reference)) {
return (isShorthandPropertyAssignment(initializer) || isPropertyAssignment(initializer)) &&
isIdentifier(initializer.name) && reference.name.escapedText === initializer.name.escapedText &&
isMatchingInitializerReference(reference.expression, initializer.parent.parent);
}
return false;
}

function getTypeAtFlowAssignment(flow: FlowAssignment) {
const node = flow.node;
// Assignments only narrow the computed type if the declared type is a union type. Thus, we
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2384,6 +2384,7 @@ namespace ts {
Shared = 1 << 10, // Referenced as antecedent more than once
PreFinally = 1 << 11, // Injected edge that links pre-finally label and pre-try flow
AfterFinally = 1 << 12, // Injected edge that links post-finally flow with the rest of the graph
Initializer = 1 << 13, // Property assignment inside object literal, intended for contextually-typed objects
Label = BranchLabel | LoopLabel,
Condition = TrueCondition | FalseCondition
}
Expand Down Expand Up @@ -2427,6 +2428,14 @@ namespace ts {
antecedent: FlowNode;
}

// FlowInitializer represents the name of a property assignment in an object literal
// that initializes a variable that has a declared type. The property assignment
// narrows the respective member of the declared type.
export interface FlowInitializer extends FlowNodeBase {
node: Identifier;
antecedent: FlowNode;
}

// FlowCondition represents a condition that is known to be true or false at the
// node's location in the control flow.
export interface FlowCondition extends FlowNodeBase {
Expand Down
5 changes: 5 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,7 @@ declare namespace ts {
Shared = 1024,
PreFinally = 2048,
AfterFinally = 4096,
Initializer = 8192,
Label = 12,
Condition = 96,
}
Expand Down Expand Up @@ -1576,6 +1577,10 @@ declare namespace ts {
node: Expression | VariableDeclaration | BindingElement;
antecedent: FlowNode;
}
interface FlowInitializer extends FlowNodeBase {
node: Identifier;
antecedent: FlowNode;
}
interface FlowCondition extends FlowNodeBase {
expression: Expression;
antecedent: FlowNode;
Expand Down
5 changes: 5 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,7 @@ declare namespace ts {
Shared = 1024,
PreFinally = 2048,
AfterFinally = 4096,
Initializer = 8192,
Label = 12,
Condition = 96,
}
Expand Down Expand Up @@ -1576,6 +1577,10 @@ declare namespace ts {
node: Expression | VariableDeclaration | BindingElement;
antecedent: FlowNode;
}
interface FlowInitializer extends FlowNodeBase {
node: Identifier;
antecedent: FlowNode;
}
interface FlowCondition extends FlowNodeBase {
expression: Expression;
antecedent: FlowNode;
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/controlFlowDeleteOperator.types
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ function f() {
>a : string | number | undefined

x.b;
>x.b : string | number
>x.b : number
>x : { a?: string | number | undefined; b: string | number; }
>b : string | number
>b : number

x.a = 1;
>x.a = 1 : 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
tests/cases/conformance/controlFlow/controlFlowObjectLiteralDeclaration.ts(30,1): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
Type 'undefined' is not assignable to type 'boolean'.
tests/cases/conformance/controlFlow/controlFlowObjectLiteralDeclaration.ts(31,1): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowObjectLiteralDeclaration.ts(32,1): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowObjectLiteralDeclaration.ts(33,1): error TS2532: Object is possibly 'undefined'.


==== tests/cases/conformance/controlFlow/controlFlowObjectLiteralDeclaration.ts (4 errors) ====
type A = {
x?: string[]
y?: number[]
z?: {
ka?: boolean
ki?: boolean
}
extra?: string
0?: string
'two words'?: string
}
// Note: spread assignments, as well as strings, numbers and computed properties,
// are not supported because they are all accessed with element access, which doesn't
// participate in control flow right now because of performance reasons.
const y = [1, 2, 3]
const wat = { extra: "life" }
let a: A = {
x: [],
y,
z: {
ka: false
},
...wat,
0: 'hi',
'two words': 'ho'
}
a.x.push('hi')
a.y.push(4)
let b = a.z.ka
b = a.z.ki // error, object is possibly undefined
~
!!! error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
!!! error TS2322: Type 'undefined' is not assignable to type 'boolean'.
a.extra.length // error, reference doesn't match the spread
~~~~~~~
!!! error TS2532: Object is possibly 'undefined'.
a[0].length // error, element access doesn't narrow
~~~~
!!! error TS2532: Object is possibly 'undefined'.
a['two words'].length
~~~~~~~~~~~~~~
!!! error TS2532: Object is possibly 'undefined'.



63 changes: 63 additions & 0 deletions tests/baselines/reference/controlFlowObjectLiteralDeclaration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//// [controlFlowObjectLiteralDeclaration.ts]
type A = {
x?: string[]
y?: number[]
z?: {
ka?: boolean
ki?: boolean
}
extra?: string
0?: string
'two words'?: string
}
// Note: spread assignments, as well as strings, numbers and computed properties,
// are not supported because they are all accessed with element access, which doesn't
// participate in control flow right now because of performance reasons.
const y = [1, 2, 3]
const wat = { extra: "life" }
let a: A = {
x: [],
y,
z: {
ka: false
},
...wat,
0: 'hi',
'two words': 'ho'
}
a.x.push('hi')
a.y.push(4)
let b = a.z.ka
b = a.z.ki // error, object is possibly undefined
a.extra.length // error, reference doesn't match the spread
a[0].length // error, element access doesn't narrow
a['two words'].length




//// [controlFlowObjectLiteralDeclaration.js]
"use strict";
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
// Note: spread assignments, as well as strings, numbers and computed properties,
// are not supported because they are all accessed with element access, which doesn't
// participate in control flow right now because of performance reasons.
var y = [1, 2, 3];
var wat = { extra: "life" };
var a = __assign({ x: [], y: y, z: {
ka: false
} }, wat, { 0: 'hi', 'two words': 'ho' });
a.x.push('hi');
a.y.push(4);
var b = a.z.ka;
b = a.z.ki; // error, object is possibly undefined
a.extra.length; // error, reference doesn't match the spread
a[0].length; // error, element access doesn't narrow
a['two words'].length;
Loading