Skip to content

Commit d43112a

Browse files
authored
Use missingType in --noUncheckedIndexedAccess mode (#51653)
* Use missingType in noUncheckedIndexedAccess mode * Accept new baselines * Add tests * Optimizing searching for undefinedType and missingType
1 parent 91f89b9 commit d43112a

5 files changed

+234
-21
lines changed

src/compiler/checker.ts

+24-21
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
18131813
const unresolvedSymbols = new Map<string, TransientSymbol>();
18141814
const errorTypes = new Map<string, Type>();
18151815

1816+
// We specifically create the `undefined` and `null` types before any other types that can occur in
1817+
// unions such that they are given low type IDs and occur first in the sorted list of union constituents.
1818+
// We can then just examine the first constituent(s) of a union to check for their presence.
1819+
18161820
const anyType = createIntrinsicType(TypeFlags.Any, "any");
18171821
const autoType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.NonInferrableType);
18181822
const wildcardType = createIntrinsicType(TypeFlags.Any, "any");
@@ -1824,8 +1828,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
18241828
const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
18251829
const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined");
18261830
const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType);
1831+
const missingType = createIntrinsicType(TypeFlags.Undefined, "undefined");
1832+
const undefinedOrMissingType = exactOptionalPropertyTypes ? missingType : undefinedType;
18271833
const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined");
1828-
const missingType = exactOptionalPropertyTypes ? createIntrinsicType(TypeFlags.Undefined, "undefined") : undefinedType;
18291834
const nullType = createIntrinsicType(TypeFlags.Null, "null");
18301835
const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null, "null", ObjectFlags.ContainsWideningType);
18311836
const stringType = createIntrinsicType(TypeFlags.String, "string");
@@ -15952,10 +15957,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1595215957
includes & TypeFlags.IncludesWildcard ? wildcardType : anyType :
1595315958
includes & TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType;
1595415959
}
15955-
if (exactOptionalPropertyTypes && includes & TypeFlags.Undefined) {
15956-
const missingIndex = binarySearch(typeSet, missingType, getTypeId, compareValues);
15957-
if (missingIndex >= 0 && containsType(typeSet, undefinedType)) {
15958-
orderedRemoveItemAt(typeSet, missingIndex);
15960+
if (includes & TypeFlags.Undefined) {
15961+
// If type set contains both undefinedType and missingType, remove missingType
15962+
if (typeSet.length >= 2 && typeSet[0] === undefinedType && typeSet[1] === missingType) {
15963+
orderedRemoveItemAt(typeSet, 1);
1595915964
}
1596015965
}
1596115966
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
@@ -16096,7 +16101,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1609616101
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
1609716102
}
1609816103
else if (strictNullChecks || !(flags & TypeFlags.Nullable)) {
16099-
if (exactOptionalPropertyTypes && type === missingType) {
16104+
if (type === missingType) {
1610016105
includes |= TypeFlags.IncludesMissingType;
1610116106
type = undefinedType;
1610216107
}
@@ -16320,9 +16325,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1632016325
result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
1632116326
}
1632216327
else if (eachIsUnionContaining(typeSet, TypeFlags.Undefined)) {
16323-
const undefinedOrMissingType = exactOptionalPropertyTypes && some(typeSet, t => containsType((t as UnionType).types, missingType)) ? missingType : undefinedType;
16328+
const containedUndefinedType = some(typeSet, containsMissingType) ? missingType : undefinedType;
1632416329
removeFromEach(typeSet, TypeFlags.Undefined);
16325-
result = getUnionType([getIntersectionType(typeSet), undefinedOrMissingType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
16330+
result = getUnionType([getIntersectionType(typeSet), containedUndefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
1632616331
}
1632716332
else if (eachIsUnionContaining(typeSet, TypeFlags.Null)) {
1632816333
removeFromEach(typeSet, TypeFlags.Null);
@@ -16853,7 +16858,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1685316858
errorIfWritingToReadonlyIndex(getIndexInfoOfType(objectType, numberType));
1685416859
return mapType(objectType, t => {
1685516860
const restType = getRestTypeOfTupleType(t as TupleTypeReference) || undefinedType;
16856-
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([restType, undefinedType]) : restType;
16861+
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([restType, missingType]) : restType;
1685716862
});
1685816863
}
1685916864
}
@@ -16875,7 +16880,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1687516880
if (accessNode && indexInfo.keyType === stringType && !isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) {
1687616881
const indexNode = getIndexNodeForAccessExpression(accessNode);
1687716882
error(indexNode, Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType));
16878-
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type;
16883+
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, missingType]) : indexInfo.type;
1687916884
}
1688016885
errorIfWritingToReadonlyIndex(indexInfo);
1688116886
// When accessing an enum object with its own type,
@@ -16887,7 +16892,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1688716892
(indexType.symbol &&
1688816893
indexType.flags & TypeFlags.EnumLiteral &&
1688916894
getParentOfSymbol(indexType.symbol) === objectType.symbol))) {
16890-
return getUnionType([indexInfo.type, undefinedType]);
16895+
return getUnionType([indexInfo.type, missingType]);
1689116896
}
1689216897
return indexInfo.type;
1689316898
}
@@ -20421,8 +20426,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2042120426
}
2042220427

2042320428
function getUndefinedStrippedTargetIfNeeded(source: Type, target: Type) {
20424-
// As a builtin type, `undefined` is a very low type ID - making it almsot always first, making this a very fast check to see
20425-
// if we need to strip `undefined` from the target
2042620429
if (source.flags & TypeFlags.Union && target.flags & TypeFlags.Union &&
2042720430
!((source as UnionType).types[0].flags & TypeFlags.Undefined) && (target as UnionType).types[0].flags & TypeFlags.Undefined) {
2042820431
return extractTypesOfKind(target, ~TypeFlags.Undefined);
@@ -22790,8 +22793,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2279022793

2279122794
function getOptionalType(type: Type, isProperty = false): Type {
2279222795
Debug.assert(strictNullChecks);
22793-
const missingOrUndefined = isProperty ? missingType : undefinedType;
22794-
return type.flags & TypeFlags.Undefined || type.flags & TypeFlags.Union && (type as UnionType).types[0] === missingOrUndefined ? type : getUnionType([type, missingOrUndefined]);
22796+
const missingOrUndefined = isProperty ? undefinedOrMissingType : undefinedType;
22797+
return type === missingOrUndefined || type.flags & TypeFlags.Union && (type as UnionType).types[0] === missingOrUndefined ? type : getUnionType([type, missingOrUndefined]);
2279522798
}
2279622799

2279722800
function getGlobalNonNullableTypeInstantiation(type: Type) {
@@ -22830,7 +22833,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2283022833
}
2283122834

2283222835
function containsMissingType(type: Type) {
22833-
return exactOptionalPropertyTypes && (type === missingType || type.flags & TypeFlags.Union && containsType((type as UnionType).types, missingType));
22836+
return type === missingType || !!(type.flags & TypeFlags.Union) && (type as UnionType).types[0] === missingType;
2283422837
}
2283522838

2283622839
function removeMissingOrUndefinedType(type: Type): Type {
@@ -22983,7 +22986,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2298322986
if (cached) {
2298422987
return cached;
2298522988
}
22986-
const result = createSymbolWithType(prop, missingType);
22989+
const result = createSymbolWithType(prop, undefinedOrMissingType);
2298722990
result.flags |= SymbolFlags.Optional;
2298822991
undefinedProperties.set(prop.escapedName, result);
2298922992
return result;
@@ -24388,7 +24391,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2438824391
}
2438924392

2439024393
function isTypeOrBaseIdenticalTo(s: Type, t: Type) {
24391-
return exactOptionalPropertyTypes && t === missingType ? s === t :
24394+
return t === missingType ? s === t :
2439224395
(isTypeIdenticalTo(s, t) || !!(t.flags & TypeFlags.String && s.flags & TypeFlags.StringLiteral || t.flags & TypeFlags.Number && s.flags & TypeFlags.NumberLiteral));
2439324396
}
2439424397

@@ -25072,7 +25075,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2507225075
function includeUndefinedInIndexSignature(type: Type | undefined): Type | undefined {
2507325076
if (!type) return type;
2507425077
return compilerOptions.noUncheckedIndexedAccess ?
25075-
getUnionType([type, undefinedType]) :
25078+
getUnionType([type, missingType]) :
2507625079
type;
2507725080
}
2507825081

@@ -29096,7 +29099,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2909629099
}
2909729100
else if (exactOptionalPropertyTypes && e.kind === SyntaxKind.OmittedExpression) {
2909829101
hasOmittedExpression = true;
29099-
elementTypes.push(missingType);
29102+
elementTypes.push(undefinedOrMissingType);
2910029103
elementFlags.push(ElementFlags.Optional);
2910129104
}
2910229105
else {
@@ -30640,7 +30643,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3064030643
error(node, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(apparentType));
3064130644
}
3064230645

30643-
propType = (compilerOptions.noUncheckedIndexedAccess && !isAssignmentTarget(node)) ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type;
30646+
propType = (compilerOptions.noUncheckedIndexedAccess && !isAssignmentTarget(node)) ? getUnionType([indexInfo.type, missingType]) : indexInfo.type;
3064430647
if (compilerOptions.noPropertyAccessFromIndexSignature && isPropertyAccessExpression(node)) {
3064530648
error(right, Diagnostics.Property_0_comes_from_an_index_signature_so_it_must_be_accessed_with_0, unescapeLeadingUnderscores(right.escapedText));
3064630649
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
=== tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts ===
2+
declare function invariant(condition: boolean): asserts condition;
3+
>invariant : Symbol(invariant, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 0))
4+
>condition : Symbol(condition, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 27))
5+
>condition : Symbol(condition, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 27))
6+
7+
function f1(obj: Record<string, string>) {
8+
>f1 : Symbol(f1, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 66))
9+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12))
10+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
11+
12+
invariant("test" in obj);
13+
>invariant : Symbol(invariant, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 0))
14+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12))
15+
16+
return obj.test; // string
17+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12))
18+
}
19+
20+
function f2(obj: Record<string, string>) {
21+
>f2 : Symbol(f2, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 5, 1))
22+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12))
23+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
24+
25+
if ("test" in obj) {
26+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12))
27+
28+
return obj.test; // string
29+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12))
30+
}
31+
return "default";
32+
}
33+
34+
function f3(obj: Record<string, string>) {
35+
>f3 : Symbol(f3, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 12, 1))
36+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
37+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
38+
39+
obj.test; // string | undefined
40+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
41+
42+
if ("test" in obj) {
43+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
44+
45+
obj.test; // string
46+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
47+
}
48+
else {
49+
obj.test; // undefined
50+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
51+
}
52+
}
53+
54+
function f4(obj: Record<string, string>) {
55+
>f4 : Symbol(f4, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 22, 1))
56+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
57+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
58+
59+
obj.test; // string | undefined
60+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
61+
62+
if (obj.hasOwnProperty("test")) {
63+
>obj.hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
64+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
65+
>hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
66+
67+
obj.test; // string
68+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
69+
}
70+
else {
71+
obj.test; // undefined
72+
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
73+
}
74+
}
75+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
=== tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts ===
2+
declare function invariant(condition: boolean): asserts condition;
3+
>invariant : (condition: boolean) => asserts condition
4+
>condition : boolean
5+
6+
function f1(obj: Record<string, string>) {
7+
>f1 : (obj: Record<string, string>) => string
8+
>obj : Record<string, string>
9+
10+
invariant("test" in obj);
11+
>invariant("test" in obj) : void
12+
>invariant : (condition: boolean) => asserts condition
13+
>"test" in obj : boolean
14+
>"test" : "test"
15+
>obj : Record<string, string>
16+
17+
return obj.test; // string
18+
>obj.test : string
19+
>obj : Record<string, string>
20+
>test : string
21+
}
22+
23+
function f2(obj: Record<string, string>) {
24+
>f2 : (obj: Record<string, string>) => string
25+
>obj : Record<string, string>
26+
27+
if ("test" in obj) {
28+
>"test" in obj : boolean
29+
>"test" : "test"
30+
>obj : Record<string, string>
31+
32+
return obj.test; // string
33+
>obj.test : string
34+
>obj : Record<string, string>
35+
>test : string
36+
}
37+
return "default";
38+
>"default" : "default"
39+
}
40+
41+
function f3(obj: Record<string, string>) {
42+
>f3 : (obj: Record<string, string>) => void
43+
>obj : Record<string, string>
44+
45+
obj.test; // string | undefined
46+
>obj.test : string | undefined
47+
>obj : Record<string, string>
48+
>test : string | undefined
49+
50+
if ("test" in obj) {
51+
>"test" in obj : boolean
52+
>"test" : "test"
53+
>obj : Record<string, string>
54+
55+
obj.test; // string
56+
>obj.test : string
57+
>obj : Record<string, string>
58+
>test : string
59+
}
60+
else {
61+
obj.test; // undefined
62+
>obj.test : undefined
63+
>obj : Record<string, string>
64+
>test : undefined
65+
}
66+
}
67+
68+
function f4(obj: Record<string, string>) {
69+
>f4 : (obj: Record<string, string>) => void
70+
>obj : Record<string, string>
71+
72+
obj.test; // string | undefined
73+
>obj.test : string | undefined
74+
>obj : Record<string, string>
75+
>test : string | undefined
76+
77+
if (obj.hasOwnProperty("test")) {
78+
>obj.hasOwnProperty("test") : boolean
79+
>obj.hasOwnProperty : (v: PropertyKey) => boolean
80+
>obj : Record<string, string>
81+
>hasOwnProperty : (v: PropertyKey) => boolean
82+
>"test" : "test"
83+
84+
obj.test; // string
85+
>obj.test : string
86+
>obj : Record<string, string>
87+
>test : string
88+
}
89+
else {
90+
obj.test; // undefined
91+
>obj.test : undefined
92+
>obj : Record<string, string>
93+
>test : undefined
94+
}
95+
}
96+

tests/baselines/reference/noUncheckedIndexedAccess.errors.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(3,32): error TS2344: Type 'boolean | undefined' does not satisfy the constraint 'boolean'.
22
Type 'undefined' is not assignable to type 'boolean'.
33
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(12,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
4+
Type 'undefined' is not assignable to type 'boolean'.
45
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(13,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
56
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(14,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
67
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(15,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
@@ -54,6 +55,7 @@ tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(99,11): error TS232
5455
const e1: boolean = strMap["foo"];
5556
~~
5657
!!! error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
58+
!!! error TS2322: Type 'undefined' is not assignable to type 'boolean'.
5759
const e2: boolean = strMap.bar;
5860
~~
5961
!!! error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// @strict: true
2+
// @noEmit: true
3+
// @noUncheckedIndexedAccess: true
4+
5+
declare function invariant(condition: boolean): asserts condition;
6+
7+
function f1(obj: Record<string, string>) {
8+
invariant("test" in obj);
9+
return obj.test; // string
10+
}
11+
12+
function f2(obj: Record<string, string>) {
13+
if ("test" in obj) {
14+
return obj.test; // string
15+
}
16+
return "default";
17+
}
18+
19+
function f3(obj: Record<string, string>) {
20+
obj.test; // string | undefined
21+
if ("test" in obj) {
22+
obj.test; // string
23+
}
24+
else {
25+
obj.test; // undefined
26+
}
27+
}
28+
29+
function f4(obj: Record<string, string>) {
30+
obj.test; // string | undefined
31+
if (obj.hasOwnProperty("test")) {
32+
obj.test; // string
33+
}
34+
else {
35+
obj.test; // undefined
36+
}
37+
}

0 commit comments

Comments
 (0)