Skip to content

Commit 1bf4f06

Browse files
authored
Merge pull request #14498 from Microsoft/narrow-default-initialised-parameters
Remove undefined from the type of default initialised parameters when narrowing
2 parents fab4ef0 + 24c8de2 commit 1bf4f06

5 files changed

+124
-36
lines changed

src/compiler/checker.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3369,16 +3369,6 @@ namespace ts {
33693369
return strictNullChecks && optional ? includeFalsyTypes(type, TypeFlags.Undefined) : type;
33703370
}
33713371

3372-
/** remove undefined from the annotated type of a parameter when there is an initializer (that doesn't include undefined) */
3373-
function removeOptionalityFromAnnotation(annotatedType: Type, declaration: VariableLikeDeclaration): Type {
3374-
const annotationIncludesUndefined = strictNullChecks &&
3375-
declaration.kind === SyntaxKind.Parameter &&
3376-
declaration.initializer &&
3377-
getFalsyFlags(annotatedType) & TypeFlags.Undefined &&
3378-
!(getFalsyFlags(checkExpression(declaration.initializer)) & TypeFlags.Undefined);
3379-
return annotationIncludesUndefined ? getNonNullableType(annotatedType) : annotatedType;
3380-
}
3381-
33823372
// Return the inferred type for a variable, parameter, or property declaration
33833373
function getTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration, includeOptionality: boolean): Type {
33843374
if (declaration.flags & NodeFlags.JavaScriptFile) {
@@ -3413,7 +3403,7 @@ namespace ts {
34133403

34143404
// Use type from type annotation if one is present
34153405
if (declaration.type) {
3416-
const declaredType = removeOptionalityFromAnnotation(getTypeFromTypeNode(declaration.type), declaration);
3406+
const declaredType = getTypeFromTypeNode(declaration.type);
34173407
return addOptionality(declaredType, /*optional*/ declaration.questionToken && includeOptionality);
34183408
}
34193409

@@ -10265,14 +10255,11 @@ namespace ts {
1026510255
return false;
1026610256
}
1026710257

10268-
function getFlowTypeOfReference(reference: Node, declaredType: Type, assumeInitialized: boolean, flowContainer: Node) {
10258+
function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType = declaredType, flowContainer?: Node, couldBeUninitialized?: boolean) {
1026910259
let key: string;
10270-
if (!reference.flowNode || assumeInitialized && !(declaredType.flags & TypeFlags.Narrowable)) {
10260+
if (!reference.flowNode || !couldBeUninitialized && !(declaredType.flags & TypeFlags.Narrowable)) {
1027110261
return declaredType;
1027210262
}
10273-
const initialType = assumeInitialized ? declaredType :
10274-
declaredType === autoType || declaredType === autoArrayType ? undefinedType :
10275-
includeFalsyTypes(declaredType, TypeFlags.Undefined);
1027610263
const visitedFlowStart = visitedFlowCount;
1027710264
const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode));
1027810265
visitedFlowCount = visitedFlowStart;
@@ -10951,6 +10938,16 @@ namespace ts {
1095110938
return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const) !== 0 && getTypeOfSymbol(symbol) !== autoArrayType;
1095210939
}
1095310940

10941+
/** remove undefined from the annotated type of a parameter when there is an initializer (that doesn't include undefined) */
10942+
function removeOptionalityFromDeclaredType(declaredType: Type, declaration: VariableLikeDeclaration): Type {
10943+
const annotationIncludesUndefined = strictNullChecks &&
10944+
declaration.kind === SyntaxKind.Parameter &&
10945+
declaration.initializer &&
10946+
getFalsyFlags(declaredType) & TypeFlags.Undefined &&
10947+
!(getFalsyFlags(checkExpression(declaration.initializer)) & TypeFlags.Undefined);
10948+
return annotationIncludesUndefined ? getTypeWithFacts(declaredType, TypeFacts.NEUndefined) : declaredType;
10949+
}
10950+
1095410951
function checkIdentifier(node: Identifier): Type {
1095510952
const symbol = getResolvedSymbol(node);
1095610953
if (symbol === unknownSymbol) {
@@ -11069,7 +11066,10 @@ namespace ts {
1106911066
const assumeInitialized = isParameter || isOuterVariable ||
1107011067
type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & TypeFlags.Any) !== 0 || isInTypeQuery(node)) ||
1107111068
isInAmbientContext(declaration);
11072-
const flowType = getFlowTypeOfReference(node, type, assumeInitialized, flowContainer);
11069+
const initialType = assumeInitialized ? (isParameter ? removeOptionalityFromDeclaredType(type, getRootDeclaration(declaration) as VariableLikeDeclaration) : type) :
11070+
type === autoType || type === autoArrayType ? undefinedType :
11071+
includeFalsyTypes(type, TypeFlags.Undefined);
11072+
const flowType = getFlowTypeOfReference(node, type, initialType, flowContainer, !assumeInitialized);
1107311073
// A variable is considered uninitialized when it is possible to analyze the entire control flow graph
1107411074
// from declaration to use, and when the variable's declared type doesn't include undefined but the
1107511075
// control flow based type does include undefined.
@@ -11335,7 +11335,7 @@ namespace ts {
1133511335
if (isClassLike(container.parent)) {
1133611336
const symbol = getSymbolOfNode(container.parent);
1133711337
const type = hasModifier(container, ModifierFlags.Static) ? getTypeOfSymbol(symbol) : (<InterfaceType>getDeclaredTypeOfSymbol(symbol)).thisType;
11338-
return getFlowTypeOfReference(node, type, /*assumeInitialized*/ true, /*flowContainer*/ undefined);
11338+
return getFlowTypeOfReference(node, type);
1133911339
}
1134011340

1134111341
if (isInJavaScriptFile(node)) {
@@ -13327,7 +13327,7 @@ namespace ts {
1332713327
!(prop.flags & SymbolFlags.Method && propType.flags & TypeFlags.Union)) {
1332813328
return propType;
1332913329
}
13330-
const flowType = getFlowTypeOfReference(node, propType, /*assumeInitialized*/ true, /*flowContainer*/ undefined);
13330+
const flowType = getFlowTypeOfReference(node, propType);
1333113331
return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType;
1333213332
}
1333313333

tests/baselines/reference/defaultParameterAddsUndefinedWithStrictNullChecks.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@ function foo2(x = "string", b: number) {
1818

1919
function foo3(x: string | undefined = "string", b: number) {
2020
x.length; // ok, should be string
21+
x = undefined;
2122
}
2223

2324
function foo4(x: string | undefined = undefined, b: number) {
2425
x; // should be string | undefined
26+
x = undefined;
2527
}
2628

29+
type OptionalNullableString = string | null | undefined;
30+
function allowsNull(val: OptionalNullableString = "") {
31+
val = null;
32+
val = 'string and null are both ok';
33+
}
34+
allowsNull(null); // still allows passing null
35+
2736

2837

2938
// .d.ts should have `string | undefined` for foo1, foo2, foo3 and foo4
@@ -72,11 +81,19 @@ function foo2(x, b) {
7281
function foo3(x, b) {
7382
if (x === void 0) { x = "string"; }
7483
x.length; // ok, should be string
84+
x = undefined;
7585
}
7686
function foo4(x, b) {
7787
if (x === void 0) { x = undefined; }
7888
x; // should be string | undefined
89+
x = undefined;
90+
}
91+
function allowsNull(val) {
92+
if (val === void 0) { val = ""; }
93+
val = null;
94+
val = 'string and null are both ok';
7995
}
96+
allowsNull(null); // still allows passing null
8097
// .d.ts should have `string | undefined` for foo1, foo2, foo3 and foo4
8198
foo1(undefined, 1);
8299
foo2(undefined, 1);
@@ -107,6 +124,8 @@ declare function foo1(x: string | undefined, b: number): void;
107124
declare function foo2(x: string | undefined, b: number): void;
108125
declare function foo3(x: string | undefined, b: number): void;
109126
declare function foo4(x: string | undefined, b: number): void;
127+
declare type OptionalNullableString = string | null | undefined;
128+
declare function allowsNull(val?: OptionalNullableString): void;
110129
declare function removeUndefinedButNotFalse(x?: boolean): false | undefined;
111130
declare const cond: boolean;
112131
declare function removeNothing(y?: boolean | undefined): boolean;

tests/baselines/reference/defaultParameterAddsUndefinedWithStrictNullChecks.symbols

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,42 @@ function foo3(x: string | undefined = "string", b: number) {
6666
>x.length : Symbol(String.length, Decl(lib.d.ts, --, --))
6767
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 17, 14))
6868
>length : Symbol(String.length, Decl(lib.d.ts, --, --))
69+
70+
x = undefined;
71+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 17, 14))
72+
>undefined : Symbol(undefined)
6973
}
7074

7175
function foo4(x: string | undefined = undefined, b: number) {
72-
>foo4 : Symbol(foo4, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 19, 1))
73-
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 21, 14))
76+
>foo4 : Symbol(foo4, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 20, 1))
77+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 22, 14))
7478
>undefined : Symbol(undefined)
75-
>b : Symbol(b, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 21, 48))
79+
>b : Symbol(b, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 22, 48))
7680

7781
x; // should be string | undefined
78-
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 21, 14))
82+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 22, 14))
83+
84+
x = undefined;
85+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 22, 14))
86+
>undefined : Symbol(undefined)
87+
}
88+
89+
type OptionalNullableString = string | null | undefined;
90+
>OptionalNullableString : Symbol(OptionalNullableString, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 25, 1))
91+
92+
function allowsNull(val: OptionalNullableString = "") {
93+
>allowsNull : Symbol(allowsNull, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 27, 56))
94+
>val : Symbol(val, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 28, 20))
95+
>OptionalNullableString : Symbol(OptionalNullableString, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 25, 1))
96+
97+
val = null;
98+
>val : Symbol(val, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 28, 20))
99+
100+
val = 'string and null are both ok';
101+
>val : Symbol(val, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 28, 20))
79102
}
103+
allowsNull(null); // still allows passing null
104+
>allowsNull : Symbol(allowsNull, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 27, 56))
80105

81106

82107

@@ -94,40 +119,40 @@ foo3(undefined, 1);
94119
>undefined : Symbol(undefined)
95120

96121
foo4(undefined, 1);
97-
>foo4 : Symbol(foo4, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 19, 1))
122+
>foo4 : Symbol(foo4, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 20, 1))
98123
>undefined : Symbol(undefined)
99124

100125

101126
function removeUndefinedButNotFalse(x = true) {
102-
>removeUndefinedButNotFalse : Symbol(removeUndefinedButNotFalse, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 31, 19))
103-
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 34, 36))
127+
>removeUndefinedButNotFalse : Symbol(removeUndefinedButNotFalse, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 40, 19))
128+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 43, 36))
104129

105130
if (x === false) {
106-
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 34, 36))
131+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 43, 36))
107132

108133
return x;
109-
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 34, 36))
134+
>x : Symbol(x, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 43, 36))
110135
}
111136
}
112137

113138
declare const cond: boolean;
114-
>cond : Symbol(cond, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 40, 13))
139+
>cond : Symbol(cond, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 49, 13))
115140

116141
function removeNothing(y = cond ? true : undefined) {
117-
>removeNothing : Symbol(removeNothing, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 40, 28))
118-
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 41, 23))
119-
>cond : Symbol(cond, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 40, 13))
142+
>removeNothing : Symbol(removeNothing, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 49, 28))
143+
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 50, 23))
144+
>cond : Symbol(cond, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 49, 13))
120145
>undefined : Symbol(undefined)
121146

122147
if (y !== undefined) {
123-
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 41, 23))
148+
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 50, 23))
124149
>undefined : Symbol(undefined)
125150

126151
if (y === false) {
127-
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 41, 23))
152+
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 50, 23))
128153

129154
return y;
130-
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 41, 23))
155+
>y : Symbol(y, Decl(defaultParameterAddsUndefinedWithStrictNullChecks.ts, 50, 23))
131156
}
132157
}
133158
return true;

tests/baselines/reference/defaultParameterAddsUndefinedWithStrictNullChecks.types

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,19 @@ function foo2(x = "string", b: number) {
8686

8787
function foo3(x: string | undefined = "string", b: number) {
8888
>foo3 : (x: string | undefined, b: number) => void
89-
>x : string
89+
>x : string | undefined
9090
>"string" : "string"
9191
>b : number
9292

9393
x.length; // ok, should be string
9494
>x.length : number
9595
>x : string
9696
>length : number
97+
98+
x = undefined;
99+
>x = undefined : undefined
100+
>x : string | undefined
101+
>undefined : undefined
97102
}
98103

99104
function foo4(x: string | undefined = undefined, b: number) {
@@ -104,7 +109,37 @@ function foo4(x: string | undefined = undefined, b: number) {
104109

105110
x; // should be string | undefined
106111
>x : string | undefined
112+
113+
x = undefined;
114+
>x = undefined : undefined
115+
>x : string | undefined
116+
>undefined : undefined
117+
}
118+
119+
type OptionalNullableString = string | null | undefined;
120+
>OptionalNullableString : OptionalNullableString
121+
>null : null
122+
123+
function allowsNull(val: OptionalNullableString = "") {
124+
>allowsNull : (val?: OptionalNullableString) => void
125+
>val : OptionalNullableString
126+
>OptionalNullableString : OptionalNullableString
127+
>"" : ""
128+
129+
val = null;
130+
>val = null : null
131+
>val : OptionalNullableString
132+
>null : null
133+
134+
val = 'string and null are both ok';
135+
>val = 'string and null are both ok' : "string and null are both ok"
136+
>val : OptionalNullableString
137+
>'string and null are both ok' : "string and null are both ok"
107138
}
139+
allowsNull(null); // still allows passing null
140+
>allowsNull(null) : void
141+
>allowsNull : (val?: OptionalNullableString) => void
142+
>null : null
108143

109144

110145

tests/cases/compiler/defaultParameterAddsUndefinedWithStrictNullChecks.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,21 @@ function foo2(x = "string", b: number) {
1919

2020
function foo3(x: string | undefined = "string", b: number) {
2121
x.length; // ok, should be string
22+
x = undefined;
2223
}
2324

2425
function foo4(x: string | undefined = undefined, b: number) {
2526
x; // should be string | undefined
27+
x = undefined;
2628
}
2729

30+
type OptionalNullableString = string | null | undefined;
31+
function allowsNull(val: OptionalNullableString = "") {
32+
val = null;
33+
val = 'string and null are both ok';
34+
}
35+
allowsNull(null); // still allows passing null
36+
2837

2938

3039
// .d.ts should have `string | undefined` for foo1, foo2, foo3 and foo4

0 commit comments

Comments
 (0)