diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 92a989a2bc707..3be8239dd8ba1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -26331,17 +26331,32 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (isKnownProperty) { // If the check is for a known property (i.e. a property declared in some constituent of // the target type), we filter the target type by presence of absence of the property. - return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); + const filtered = filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); + // without `noUncheckedIndexedAccess` we can't narrow any further + // `{ [x: PropertyKey]: T }[prop]` already returns `T` + if (!assumeTrue || !compilerOptions.noUncheckedIndexedAccess) { + return filtered; + } + return mapType(filtered, t => { + if (getPropertyOfType(type, name)) { + return t; + } + return intersectWithNarrowingRecordType(type, nameType, getApplicableIndexInfoForName(type, name)!.type); + }); } if (assumeTrue) { // If the check is for an unknown property, we intersect the target type with `Record`, // where X is the name of the property. - const recordSymbol = getGlobalRecordSymbol(); - if (recordSymbol) { - return getIntersectionType([type, getTypeAliasInstantiation(recordSymbol, [nameType, unknownType])]); - } + return intersectWithNarrowingRecordType(type, nameType, unknownType); } return type; + + function intersectWithNarrowingRecordType(type: Type, nameType: StringLiteralType | NumberLiteralType | UniqueESSymbolType, propType: Type) { + const recordSymbol = getGlobalRecordSymbol(); + return recordSymbol + ? getIntersectionType([type, getTypeAliasInstantiation(recordSymbol, [nameType, propType])]) + : type; + } } function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { diff --git a/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols new file mode 100644 index 0000000000000..9a65d9b26a631 --- /dev/null +++ b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols @@ -0,0 +1,56 @@ +=== 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) { +>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 +>obj.test : Symbol(test) +>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 2, 12)) +>test : Symbol(test) +} + +function f2(obj: Record) { +>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 +>obj.test : Symbol(test) +>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 7, 12)) +>test : Symbol(test) + } + return "default" +} + +function f3(obj: Record) { +>f3 : Symbol(f3, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 12, 1)) +>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + if ("test" in obj) { +>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12)) + + obj.test +>obj.test : Symbol(test) +>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12)) +>test : Symbol(test) + } + else { + obj.test +>obj : Symbol(obj, Decl(inKeywordNarrowingWithNoUncheckedIndexedAccess.ts, 14, 12)) + } +} + diff --git a/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types new file mode 100644 index 0000000000000..587df1a22783d --- /dev/null +++ b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types @@ -0,0 +1,62 @@ +=== tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts === +declare function invariant(condition: boolean): asserts condition; +>invariant : (condition: boolean) => asserts condition +>condition : boolean + +function f1(obj: Record) { +>f1 : (obj: Record) => string +>obj : Record + + invariant("test" in obj); +>invariant("test" in obj) : void +>invariant : (condition: boolean) => asserts condition +>"test" in obj : boolean +>"test" : "test" +>obj : Record + + return obj.test +>obj.test : string +>obj : Record & Record<"test", string> +>test : string +} + +function f2(obj: Record) { +>f2 : (obj: Record) => string +>obj : Record + + if ("test" in obj) { +>"test" in obj : boolean +>"test" : "test" +>obj : Record + + return obj.test +>obj.test : string +>obj : Record & Record<"test", string> +>test : string + } + return "default" +>"default" : "default" +} + +function f3(obj: Record) { +>f3 : (obj: Record) => void +>obj : Record + + if ("test" in obj) { +>"test" in obj : boolean +>"test" : "test" +>obj : Record + + obj.test +>obj.test : string +>obj : Record & Record<"test", string> +>test : string + } + else { + obj.test +>obj.test : string | undefined +>obj : Record +>test : string | undefined + } +} + diff --git a/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts b/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts new file mode 100644 index 0000000000000..027cc183dd59c --- /dev/null +++ b/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts @@ -0,0 +1,26 @@ +// @strict: true +// @noEmit: true +// @noUncheckedIndexedAccess: true + +declare function invariant(condition: boolean): asserts condition; + +function f1(obj: Record) { + invariant("test" in obj); + return obj.test +} + +function f2(obj: Record) { + if ("test" in obj) { + return obj.test + } + return "default" +} + +function f3(obj: Record) { + if ("test" in obj) { + obj.test + } + else { + obj.test + } +}