Skip to content
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

Use missingType in --noUncheckedIndexedAccess mode #51653

Merged
merged 4 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 16 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1823,7 +1823,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined");
const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType);
const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined");
const missingType = exactOptionalPropertyTypes ? createIntrinsicType(TypeFlags.Undefined, "undefined") : undefinedType;
const missingType = createIntrinsicType(TypeFlags.Undefined, "undefined");
const undefinedOrMissingType = exactOptionalPropertyTypes ? missingType : undefinedType;
const nullType = createIntrinsicType(TypeFlags.Null, "null");
const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null, "null", ObjectFlags.ContainsWideningType);
const stringType = createIntrinsicType(TypeFlags.String, "string");
Expand Down Expand Up @@ -15952,7 +15953,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
includes & TypeFlags.IncludesWildcard ? wildcardType : anyType :
includes & TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType;
}
if (exactOptionalPropertyTypes && includes & TypeFlags.Undefined) {
if (includes & TypeFlags.Undefined) {
const missingIndex = binarySearch(typeSet, missingType, getTypeId, compareValues);
if (missingIndex >= 0 && containsType(typeSet, undefinedType)) {
orderedRemoveItemAt(typeSet, missingIndex);
Expand Down Expand Up @@ -16096,7 +16097,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
}
else if (strictNullChecks || !(flags & TypeFlags.Nullable)) {
if (exactOptionalPropertyTypes && type === missingType) {
if (type === missingType) {
includes |= TypeFlags.IncludesMissingType;
type = undefinedType;
}
Expand Down Expand Up @@ -16320,9 +16321,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
}
else if (eachIsUnionContaining(typeSet, TypeFlags.Undefined)) {
const undefinedOrMissingType = exactOptionalPropertyTypes && some(typeSet, t => containsType((t as UnionType).types, missingType)) ? missingType : undefinedType;
const containedUndefinedType = some(typeSet, t => containsType((t as UnionType).types, missingType)) ? missingType : undefinedType;
removeFromEach(typeSet, TypeFlags.Undefined);
result = getUnionType([getIntersectionType(typeSet), undefinedOrMissingType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
result = getUnionType([getIntersectionType(typeSet), containedUndefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
}
else if (eachIsUnionContaining(typeSet, TypeFlags.Null)) {
removeFromEach(typeSet, TypeFlags.Null);
Expand Down Expand Up @@ -16853,7 +16854,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
errorIfWritingToReadonlyIndex(getIndexInfoOfType(objectType, numberType));
return mapType(objectType, t => {
const restType = getRestTypeOfTupleType(t as TupleTypeReference) || undefinedType;
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([restType, undefinedType]) : restType;
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([restType, missingType]) : restType;
});
}
}
Expand All @@ -16875,7 +16876,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (accessNode && indexInfo.keyType === stringType && !isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) {
const indexNode = getIndexNodeForAccessExpression(accessNode);
error(indexNode, Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType));
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type;
return accessFlags & AccessFlags.IncludeUndefined ? getUnionType([indexInfo.type, missingType]) : indexInfo.type;
}
errorIfWritingToReadonlyIndex(indexInfo);
// When accessing an enum object with its own type,
Expand All @@ -16887,7 +16888,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
(indexType.symbol &&
indexType.flags & TypeFlags.EnumLiteral &&
getParentOfSymbol(indexType.symbol) === objectType.symbol))) {
return getUnionType([indexInfo.type, undefinedType]);
return getUnionType([indexInfo.type, missingType]);
}
return indexInfo.type;
}
Expand Down Expand Up @@ -22783,7 +22784,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function getOptionalType(type: Type, isProperty = false): Type {
Debug.assert(strictNullChecks);
const missingOrUndefined = isProperty ? missingType : undefinedType;
const missingOrUndefined = isProperty ? undefinedOrMissingType : undefinedType;
return type.flags & TypeFlags.Undefined || type.flags & TypeFlags.Union && (type as UnionType).types[0] === missingOrUndefined ? type : getUnionType([type, missingOrUndefined]);
}

Expand Down Expand Up @@ -22823,7 +22824,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function containsMissingType(type: Type) {
return exactOptionalPropertyTypes && (type === missingType || type.flags & TypeFlags.Union && containsType((type as UnionType).types, missingType));
return type === missingType || type.flags & TypeFlags.Union && containsType((type as UnionType).types, missingType);
}

function removeMissingOrUndefinedType(type: Type): Type {
Expand Down Expand Up @@ -22976,7 +22977,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (cached) {
return cached;
}
const result = createSymbolWithType(prop, missingType);
const result = createSymbolWithType(prop, undefinedOrMissingType);
result.flags |= SymbolFlags.Optional;
undefinedProperties.set(prop.escapedName, result);
return result;
Expand Down Expand Up @@ -24409,7 +24410,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function isTypeOrBaseIdenticalTo(s: Type, t: Type) {
return exactOptionalPropertyTypes && t === missingType ? s === t :
return t === missingType ? s === t :
(isTypeIdenticalTo(s, t) || !!(t.flags & TypeFlags.String && s.flags & TypeFlags.StringLiteral || t.flags & TypeFlags.Number && s.flags & TypeFlags.NumberLiteral));
}

Expand Down Expand Up @@ -25093,7 +25094,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function includeUndefinedInIndexSignature(type: Type | undefined): Type | undefined {
if (!type) return type;
return compilerOptions.noUncheckedIndexedAccess ?
getUnionType([type, undefinedType]) :
getUnionType([type, missingType]) :
type;
}

Expand Down Expand Up @@ -29117,7 +29118,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
else if (exactOptionalPropertyTypes && e.kind === SyntaxKind.OmittedExpression) {
hasOmittedExpression = true;
elementTypes.push(missingType);
elementTypes.push(undefinedOrMissingType);
elementFlags.push(ElementFlags.Optional);
}
else {
Expand Down Expand Up @@ -30661,7 +30662,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
error(node, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(apparentType));
}

propType = (compilerOptions.noUncheckedIndexedAccess && !isAssignmentTarget(node)) ? getUnionType([indexInfo.type, undefinedType]) : indexInfo.type;
propType = (compilerOptions.noUncheckedIndexedAccess && !isAssignmentTarget(node)) ? getUnionType([indexInfo.type, missingType]) : indexInfo.type;
if (compilerOptions.noPropertyAccessFromIndexSignature && isPropertyAccessExpression(node)) {
error(right, Diagnostics.Property_0_comes_from_an_index_signature_so_it_must_be_accessed_with_0, unescapeLeadingUnderscores(right.escapedText));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
=== tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts ===
declare function invariant(condition: boolean): asserts condition;
>invariant : Symbol(invariant, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 0))
>condition : Symbol(condition, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 27))
>condition : Symbol(condition, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 27))

function f1(obj: Record<string, string>) {
>f1 : Symbol(f1, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 66))
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))

invariant("test" in obj);
>invariant : Symbol(invariant, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 0, 0))
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12))

return obj.test; // string
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12))
}

function f2(obj: Record<string, string>) {
>f2 : Symbol(f2, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 5, 1))
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))

if ("test" in obj) {
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12))

return obj.test; // string
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12))
}
return "default";
}

function f3(obj: Record<string, string>) {
>f3 : Symbol(f3, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 12, 1))
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))

obj.test; // string | undefined
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))

if ("test" in obj) {
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))

obj.test; // string
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
}
else {
obj.test; // undefined
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12))
}
}

function f4(obj: Record<string, string>) {
>f4 : Symbol(f4, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 22, 1))
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))

obj.test; // string | undefined
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))

if (obj.hasOwnProperty("test")) {
>obj.hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
>hasOwnProperty : Symbol(Object.hasOwnProperty, Decl(lib.es5.d.ts, --, --))

obj.test; // string
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
}
else {
obj.test; // undefined
>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 24, 12))
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
=== tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts ===
declare function invariant(condition: boolean): asserts condition;
>invariant : (condition: boolean) => asserts condition
>condition : boolean

function f1(obj: Record<string, string>) {
>f1 : (obj: Record<string, string>) => string
>obj : Record<string, string>

invariant("test" in obj);
>invariant("test" in obj) : void
>invariant : (condition: boolean) => asserts condition
>"test" in obj : boolean
>"test" : "test"
>obj : Record<string, string>

return obj.test; // string
>obj.test : string
>obj : Record<string, string>
>test : string
}

function f2(obj: Record<string, string>) {
>f2 : (obj: Record<string, string>) => string
>obj : Record<string, string>

if ("test" in obj) {
>"test" in obj : boolean
>"test" : "test"
>obj : Record<string, string>

return obj.test; // string
>obj.test : string
>obj : Record<string, string>
>test : string
}
return "default";
>"default" : "default"
}

function f3(obj: Record<string, string>) {
>f3 : (obj: Record<string, string>) => void
>obj : Record<string, string>

obj.test; // string | undefined
>obj.test : string | undefined
>obj : Record<string, string>
>test : string | undefined

if ("test" in obj) {
>"test" in obj : boolean
>"test" : "test"
>obj : Record<string, string>

obj.test; // string
>obj.test : string
>obj : Record<string, string>
>test : string
}
else {
obj.test; // undefined
>obj.test : undefined
>obj : Record<string, string>
>test : undefined
}
}

function f4(obj: Record<string, string>) {
>f4 : (obj: Record<string, string>) => void
>obj : Record<string, string>

obj.test; // string | undefined
>obj.test : string | undefined
>obj : Record<string, string>
>test : string | undefined

if (obj.hasOwnProperty("test")) {
>obj.hasOwnProperty("test") : boolean
>obj.hasOwnProperty : (v: PropertyKey) => boolean
>obj : Record<string, string>
>hasOwnProperty : (v: PropertyKey) => boolean
>"test" : "test"

obj.test; // string
>obj.test : string
>obj : Record<string, string>
>test : string
}
else {
obj.test; // undefined
>obj.test : undefined
>obj : Record<string, string>
>test : undefined
}
}

2 changes: 2 additions & 0 deletions tests/baselines/reference/noUncheckedIndexedAccess.errors.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(3,32): error TS2344: Type 'boolean | undefined' does not satisfy the constraint 'boolean'.
Type 'undefined' is not assignable to type 'boolean'.
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(12,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
Type 'undefined' is not assignable to type 'boolean'.
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(13,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(14,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(15,7): error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
Expand Down Expand Up @@ -54,6 +55,7 @@ tests/cases/conformance/pedantic/noUncheckedIndexedAccess.ts(99,11): error TS232
const e1: boolean = strMap["foo"];
~~
!!! error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
!!! error TS2322: Type 'undefined' is not assignable to type 'boolean'.
const e2: boolean = strMap.bar;
~~
!!! error TS2322: Type 'boolean | undefined' is not assignable to type 'boolean'.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// @strict: true
// @noEmit: true
// @noUncheckedIndexedAccess: true

declare function invariant(condition: boolean): asserts condition;

function f1(obj: Record<string, string>) {
invariant("test" in obj);
return obj.test; // string
}

function f2(obj: Record<string, string>) {
if ("test" in obj) {
return obj.test; // string
}
return "default";
}

function f3(obj: Record<string, string>) {
obj.test; // string | undefined
if ("test" in obj) {
obj.test; // string
}
else {
obj.test; // undefined
}
}

function f4(obj: Record<string, string>) {
obj.test; // string | undefined
if (obj.hasOwnProperty("test")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, how does this one work if hasOwnProperty's signature is "just":

hasOwnProperty(v: PropertyKey): boolean;

what kind of sorcery is going on here? :D does this method have some special support in the compiler itself?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this method have some special support in the compiler itself?

It does indeed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m going to assume that feature was implemented early on since nowadays feature requests to special-case built-in methods (like Object.assign) get rejected if the special case can’t be represented in the method signature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, I found the code related to hasOwnProperty here

obj.test; // string
}
else {
obj.test; // undefined
}
}
Copy link
Contributor

@Andarist Andarist Nov 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've hoped that this solution would make it work (somehow through CFA) for at least the in variant:

function f5(obj: Record<string, string>, prop: string) {
    if (obj[prop]) {
        obj[prop]; // actual: string | undefined; expected: string
    }
    if (prop in obj) {
        obj[prop]; // actual: string | undefined; expected: string
    }
}

Is this kind of analysis supported for any kind of patterns today?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obj[prop] where prop isn’t a hardcoded literal can’t be narrowed in any way to my knowledge. There are many, many issues attesting to that if you go looking, and it’s considered to be by design.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye, I've figured out as much - although I wonder how hard a limitation that is.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything gets duped to #10530 which is kind of misleading since that issue was originally about bracketed access with a literal key (which has since been fixed), but the picture that develops after enough hunting is that it’s a performance sinkhole. See e.g. #25109 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Andarist It works when prop is declared as a const with a singleton literal type, e.g. const prop = "foo", but no support beyond that. We could conceivably support non-singleton-literal-type const variables, but it isn't trivial.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many, many issues attesting to that if you go looking, and it’s considered to be by design.

I understand that this seems difficult to implement and we might never get this feature, but I'm having a hard time understanding how this could be "by design". What I mean is, this seems like fairly straight forward JS and as far as I understand TypeScript's design goals, we really should be able to support (type correctly) this kind of code. I really am sympathetic to the fact that it might not be feasible to do in the current implementation, I'm just challenging the idea that we want it to be this way.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“By design” = ”intentional decision made during the design process”. Some such decisions are known compromises. I don’t use the phrase to imply it’s what the user wants, because nobody can even define that except for the individual users themselves, and the team can’t go around asking every single user before they’re allowed to label things “working as intended”. 😉

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure there always needs to be compromises made in the design of the language, but this isn't that. This seems to be a limitation of the current implementation (an implementation detail if you will), not some inherent limitation in the design of the language itself.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, the implementation is still part of the design of the TypeScript compiler in particular. Obviously a language wouldn't be intentionally designed that way in isolation, but that's a moot point since there's no TypeScript language specification. There used to be a formal spec a long time ago, but nowadays the implementation is the specification, and that's an entirely pragmatic design process. If the implementation is designed a certain way, in practice that means the language is too.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...at least until someone gets fed up with the limitations and makes an alternative implementation. 😅