From ec7b6561bc61c90f72c32e20d2d00d276b5b83d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 24 Sep 2022 11:13:03 +0200 Subject: [PATCH 1/3] Improve checking of `in` operator combined with `noUncheckedIndexedAccess` --- src/compiler/checker.ts | 26 +++++++++--- ...rowingWithNoUncheckedIndexedAccess.symbols | 39 +++++++++++++++++ ...arrowingWithNoUncheckedIndexedAccess.types | 42 +++++++++++++++++++ ...rdNarrowingWithNoUncheckedIndexedAccess.ts | 19 +++++++++ 4 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols create mode 100644 tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types create mode 100644 tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index dd56dc8ff8905..968b73bdc848a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -25251,17 +25251,33 @@ namespace ts { 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` + // `{ prop?: T }[prop]` always implies `T | undefined` + if (!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..813cafdecc575 --- /dev/null +++ b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols @@ -0,0 +1,39 @@ +=== 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" +} + + + diff --git a/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types new file mode 100644 index 0000000000000..99f92ee8eef6e --- /dev/null +++ b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types @@ -0,0 +1,42 @@ +=== 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" +} + + + diff --git a/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts b/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts new file mode 100644 index 0000000000000..76040f2c97307 --- /dev/null +++ b/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts @@ -0,0 +1,19 @@ +// @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" +} + + From e0bca8c0d1ae0a14147073aa885c5669ae7106d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 24 Sep 2022 21:42:04 +0200 Subject: [PATCH 2/3] Removed part of the comment about optional property - as that is not affected by `noUncheckedIndexedAccess` at all --- src/compiler/checker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 968b73bdc848a..c4d04e8af29a1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -25254,7 +25254,6 @@ namespace ts { const filtered = filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); // without `noUncheckedIndexedAccess` we can't narrow any further // `{ [x: PropertyKey]: T }[prop]` already returns `T` - // `{ prop?: T }[prop]` always implies `T | undefined` if (!compilerOptions.noUncheckedIndexedAccess) { return filtered; } From f1fe5c54f12ae537f3b57aa6f632c1ab16c0d9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 26 Nov 2022 08:22:27 +0100 Subject: [PATCH 3/3] Dont narrow outside of the `assumeTrue` --- src/compiler/checker.ts | 2 +- ...rowingWithNoUncheckedIndexedAccess.symbols | 17 ++++++++++++++++ ...arrowingWithNoUncheckedIndexedAccess.types | 20 +++++++++++++++++++ ...rdNarrowingWithNoUncheckedIndexedAccess.ts | 9 ++++++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 315f4c2aa55b6..3be8239dd8ba1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -26334,7 +26334,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { 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 (!compilerOptions.noUncheckedIndexedAccess) { + if (!assumeTrue || !compilerOptions.noUncheckedIndexedAccess) { return filtered; } return mapType(filtered, t => { diff --git a/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols index 813cafdecc575..9a65d9b26a631 100644 --- a/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols +++ b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.symbols @@ -35,5 +35,22 @@ function f2(obj: Record) { 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 index 99f92ee8eef6e..587df1a22783d 100644 --- a/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types +++ b/tests/baselines/reference/inKeywordNarrowingWithNoUncheckedIndexedAccess.types @@ -38,5 +38,25 @@ function f2(obj: Record) { >"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 index 76040f2c97307..027cc183dd59c 100644 --- a/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts +++ b/tests/cases/compiler/inKeywordNarrowingWithNoUncheckedIndexedAccess.ts @@ -16,4 +16,11 @@ function f2(obj: Record) { return "default" } - +function f3(obj: Record) { + if ("test" in obj) { + obj.test + } + else { + obj.test + } +}