Skip to content

Commit 66fa9f6

Browse files
authored
Just map type variables to constraints at certain positions for narrowing so that we do not map primitives (#21384)
* Use a limited version of getApparentType that doesnt map primitives * Reuse [most of] getBaseConstraintOfType, since it does the needed behaviors * Move new function next to the very similar function
1 parent 8a52ead commit 66fa9f6

5 files changed

+122
-11
lines changed

src/compiler/checker.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4015,7 +4015,7 @@ namespace ts {
40154015
parentType = getNonNullableType(parentType);
40164016
}
40174017
const propType = getTypeOfPropertyOfType(parentType, text);
4018-
const declaredType = propType && getApparentTypeForLocation(propType, declaration.name);
4018+
const declaredType = propType && getConstraintForLocation(propType, declaration.name);
40194019
type = declaredType && getFlowTypeOfReference(declaration, declaredType) ||
40204020
isNumericLiteralName(text) && getIndexTypeOfType(parentType, IndexKind.Number) ||
40214021
getIndexTypeOfType(parentType, IndexKind.String);
@@ -6138,17 +6138,29 @@ namespace ts {
61386138
return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type);
61396139
}
61406140

6141-
function getBaseConstraintOfType(type: Type): Type {
6141+
function getBaseConstraintOfInstantiableNonPrimitiveUnionOrIntersection(type: Type) {
61426142
if (type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.UnionOrIntersection)) {
61436143
const constraint = getResolvedBaseConstraint(<InstantiableType | UnionOrIntersectionType>type);
61446144
if (constraint !== noConstraintType && constraint !== circularConstraintType) {
61456145
return constraint;
61466146
}
61476147
}
6148-
else if (type.flags & TypeFlags.Index) {
6148+
}
6149+
6150+
function getBaseConstraintOfType(type: Type): Type {
6151+
const constraint = getBaseConstraintOfInstantiableNonPrimitiveUnionOrIntersection(type);
6152+
if (!constraint && type.flags & TypeFlags.Index) {
61496153
return stringType;
61506154
}
6151-
return undefined;
6155+
return constraint;
6156+
}
6157+
6158+
/**
6159+
* This is similar to `getBaseConstraintOfType` except it returns the input type if there's no base constraint, instead of `undefined`
6160+
* It also doesn't map indexes to `string`, as where this is used this would be unneeded (and likely undesirable)
6161+
*/
6162+
function getBaseConstraintOrType(type: Type) {
6163+
return getBaseConstraintOfType(type) || type;
61526164
}
61536165

61546166
function hasNonCircularBaseConstraint(type: InstantiableType): boolean {
@@ -11935,7 +11947,7 @@ namespace ts {
1193511947
function getFlowCacheKey(node: Node): string | undefined {
1193611948
if (node.kind === SyntaxKind.Identifier) {
1193711949
const symbol = getResolvedSymbol(<Identifier>node);
11938-
return symbol !== unknownSymbol ? (isApparentTypePosition(node) ? "@" : "") + getSymbolId(symbol) : undefined;
11950+
return symbol !== unknownSymbol ? (isConstraintPosition(node) ? "@" : "") + getSymbolId(symbol) : undefined;
1193911951
}
1194011952
if (node.kind === SyntaxKind.ThisKeyword) {
1194111953
return "0";
@@ -13297,7 +13309,7 @@ namespace ts {
1329713309
return annotationIncludesUndefined ? getTypeWithFacts(declaredType, TypeFacts.NEUndefined) : declaredType;
1329813310
}
1329913311

13300-
function isApparentTypePosition(node: Node) {
13312+
function isConstraintPosition(node: Node) {
1330113313
const parent = node.parent;
1330213314
return parent.kind === SyntaxKind.PropertyAccessExpression ||
1330313315
parent.kind === SyntaxKind.CallExpression && (<CallExpression>parent).expression === node ||
@@ -13310,13 +13322,13 @@ namespace ts {
1331013322
return type.flags & TypeFlags.InstantiableNonPrimitive && maybeTypeOfKind(getBaseConstraintOfType(type) || emptyObjectType, TypeFlags.Nullable);
1331113323
}
1331213324

13313-
function getApparentTypeForLocation(type: Type, node: Node) {
13325+
function getConstraintForLocation(type: Type, node: Node) {
1331413326
// When a node is the left hand expression of a property access, element access, or call expression,
1331513327
// and the type of the node includes type variables with constraints that are nullable, we fetch the
1331613328
// apparent type of the node *before* performing control flow analysis such that narrowings apply to
1331713329
// the constraint type.
13318-
if (isApparentTypePosition(node) && forEachType(type, typeHasNullableConstraint)) {
13319-
return mapType(getWidenedType(type), getApparentType);
13330+
if (isConstraintPosition(node) && forEachType(type, typeHasNullableConstraint)) {
13331+
return mapType(getWidenedType(type), getBaseConstraintOrType);
1332013332
}
1332113333
return type;
1332213334
}
@@ -13404,7 +13416,7 @@ namespace ts {
1340413416
checkCollisionWithCapturedNewTargetVariable(node, node);
1340513417
checkNestedBlockScopedBinding(node, symbol);
1340613418

13407-
const type = getApparentTypeForLocation(getTypeOfSymbol(localOrExportSymbol), node);
13419+
const type = getConstraintForLocation(getTypeOfSymbol(localOrExportSymbol), node);
1340813420
const assignmentKind = getAssignmentTargetKind(node);
1340913421

1341013422
if (assignmentKind) {
@@ -16024,7 +16036,7 @@ namespace ts {
1602416036
return unknownType;
1602516037
}
1602616038
}
16027-
propType = getApparentTypeForLocation(getTypeOfSymbol(prop), node);
16039+
propType = getConstraintForLocation(getTypeOfSymbol(prop), node);
1602816040
}
1602916041
// Only compute control flow type if this is a property access expression that isn't an
1603016042
// assignment target, and the referenced property was declared as a variable, property,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//// [nonNullParameterExtendingStringAssignableToString.ts]
2+
declare function foo(p: string): void;
3+
4+
function fn<T extends string | undefined, U extends string>(one: T, two: U) {
5+
let three = Boolean() ? one : two;
6+
foo(one!);
7+
foo(two!);
8+
foo(three!); // this line is the important one
9+
}
10+
11+
//// [nonNullParameterExtendingStringAssignableToString.js]
12+
"use strict";
13+
function fn(one, two) {
14+
var three = Boolean() ? one : two;
15+
foo(one);
16+
foo(two);
17+
foo(three); // this line is the important one
18+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
=== tests/cases/compiler/nonNullParameterExtendingStringAssignableToString.ts ===
2+
declare function foo(p: string): void;
3+
>foo : Symbol(foo, Decl(nonNullParameterExtendingStringAssignableToString.ts, 0, 0))
4+
>p : Symbol(p, Decl(nonNullParameterExtendingStringAssignableToString.ts, 0, 21))
5+
6+
function fn<T extends string | undefined, U extends string>(one: T, two: U) {
7+
>fn : Symbol(fn, Decl(nonNullParameterExtendingStringAssignableToString.ts, 0, 38))
8+
>T : Symbol(T, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 12))
9+
>U : Symbol(U, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 41))
10+
>one : Symbol(one, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 60))
11+
>T : Symbol(T, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 12))
12+
>two : Symbol(two, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 67))
13+
>U : Symbol(U, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 41))
14+
15+
let three = Boolean() ? one : two;
16+
>three : Symbol(three, Decl(nonNullParameterExtendingStringAssignableToString.ts, 3, 7))
17+
>Boolean : Symbol(Boolean, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
18+
>one : Symbol(one, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 60))
19+
>two : Symbol(two, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 67))
20+
21+
foo(one!);
22+
>foo : Symbol(foo, Decl(nonNullParameterExtendingStringAssignableToString.ts, 0, 0))
23+
>one : Symbol(one, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 60))
24+
25+
foo(two!);
26+
>foo : Symbol(foo, Decl(nonNullParameterExtendingStringAssignableToString.ts, 0, 0))
27+
>two : Symbol(two, Decl(nonNullParameterExtendingStringAssignableToString.ts, 2, 67))
28+
29+
foo(three!); // this line is the important one
30+
>foo : Symbol(foo, Decl(nonNullParameterExtendingStringAssignableToString.ts, 0, 0))
31+
>three : Symbol(three, Decl(nonNullParameterExtendingStringAssignableToString.ts, 3, 7))
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
=== tests/cases/compiler/nonNullParameterExtendingStringAssignableToString.ts ===
2+
declare function foo(p: string): void;
3+
>foo : (p: string) => void
4+
>p : string
5+
6+
function fn<T extends string | undefined, U extends string>(one: T, two: U) {
7+
>fn : <T extends string | undefined, U extends string>(one: T, two: U) => void
8+
>T : T
9+
>U : U
10+
>one : T
11+
>T : T
12+
>two : U
13+
>U : U
14+
15+
let three = Boolean() ? one : two;
16+
>three : T | U
17+
>Boolean() ? one : two : T | U
18+
>Boolean() : boolean
19+
>Boolean : BooleanConstructor
20+
>one : T
21+
>two : U
22+
23+
foo(one!);
24+
>foo(one!) : void
25+
>foo : (p: string) => void
26+
>one! : string
27+
>one : string | undefined
28+
29+
foo(two!);
30+
>foo(two!) : void
31+
>foo : (p: string) => void
32+
>two! : U
33+
>two : U
34+
35+
foo(three!); // this line is the important one
36+
>foo(three!) : void
37+
>foo : (p: string) => void
38+
>three! : string
39+
>three : string
40+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// @strict: true
2+
declare function foo(p: string): void;
3+
4+
function fn<T extends string | undefined, U extends string>(one: T, two: U) {
5+
let three = Boolean() ? one : two;
6+
foo(one!);
7+
foo(two!);
8+
foo(three!); // this line is the important one
9+
}

0 commit comments

Comments
 (0)