From b3d3ec9944b93bbd1a909f63ba17592742e70335 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 26 Feb 2023 08:20:34 -1000 Subject: [PATCH 1/7] Use strictSubtypeRelation in getNarrowedType and narrow only for pure subtypes --- src/compiler/checker.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 70e0220bbb39c..0db9c3565beae 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -19108,6 +19108,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return isTypeRelatedTo(source, target, subtypeRelation); } + function isTypeStrictSubtypeOf(source: Type, target: Type): boolean { + return isTypeRelatedTo(source, target, strictSubtypeRelation); + } + function isTypeAssignableTo(source: Type, target: Type): boolean { return isTypeRelatedTo(source, target, assignableRelation); } @@ -27250,7 +27254,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // We first attempt to filter the current type, narrowing constituents as appropriate and removing // constituents that are unrelated to the candidate. - const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf; + const isRelated = checkDerived ? isTypeDerivedFrom : isTypeStrictSubtypeOf; const keyPropertyName = type.flags & TypeFlags.Union ? getKeyPropertyName(type as UnionType) : undefined; const narrowedType = mapType(candidate, c => { // If a discriminant property is available, use that to reduce the type. @@ -27260,9 +27264,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // specific of the two. When t and c are related in both directions, we prefer c for type predicates // because that is the asserted type, but t for `instanceof` because generics aren't reflected in // prototype object types. - const directlyRelated = mapType(matching || type, checkDerived ? - t => isTypeDerivedFrom(t, c) ? t : isTypeDerivedFrom(c, t) ? c : neverType : - t => isTypeSubtypeOf(c, t) && !isTypeIdenticalTo(c, t) ? c : isTypeSubtypeOf(t, c) ? t : neverType); + const directlyRelated = mapType(matching || type, t => isRelated(t, c) ? t : isRelated(c, t) ? c : neverType); // If no constituents are directly related, create intersections for any generic constituents that // are related by constraint. return directlyRelated.flags & TypeFlags.Never ? @@ -27272,7 +27274,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // If filtering produced a non-empty type, return that. Otherwise, pick the most specific of the two // based on assignability, or as a last resort produce an intersection. return !(narrowedType.flags & TypeFlags.Never) ? narrowedType : - isTypeSubtypeOf(candidate, type) ? candidate : + isTypeStrictSubtypeOf(candidate, type) ? candidate : isTypeAssignableTo(type, candidate) ? type : isTypeAssignableTo(candidate, type) ? candidate : getIntersectionType([type, candidate]); From 0325f9dbd92f6f8595d8e91d97cacd9ff884f2b1 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 26 Feb 2023 08:21:44 -1000 Subject: [PATCH 2/7] Accept new baselines --- ...ssertedTypeThroughTypePredicate.errors.txt | 51 ------------------- ...avorAssertedTypeThroughTypePredicate.types | 12 ++--- .../reference/narrowingMutualSubtypes.types | 4 +- ...uardNarrowByMutableUntypedField.errors.txt | 11 ++++ ...typeGuardNarrowByMutableUntypedField.types | 6 +-- 5 files changed, 22 insertions(+), 62 deletions(-) delete mode 100644 tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.errors.txt create mode 100644 tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt diff --git a/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.errors.txt b/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.errors.txt deleted file mode 100644 index 4ae99c4108d14..0000000000000 --- a/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.errors.txt +++ /dev/null @@ -1,51 +0,0 @@ -tests/cases/compiler/controlFlowFavorAssertedTypeThroughTypePredicate.ts(26,5): error TS7053: Element implicitly has an 'any' type because expression of type '"attr"' can't be used to index type '{}'. - Property 'attr' does not exist on type '{}'. -tests/cases/compiler/controlFlowFavorAssertedTypeThroughTypePredicate.ts(34,5): error TS7053: Element implicitly has an 'any' type because expression of type '"attr"' can't be used to index type '{}'. - Property 'attr' does not exist on type '{}'. - - -==== tests/cases/compiler/controlFlowFavorAssertedTypeThroughTypePredicate.ts (2 errors) ==== - // repro 49988#issuecomment-1192016929 - - declare function isObject1(value: unknown): value is Record; - - declare const obj1: {}; - if (isObject1(obj1)) { - obj1; - obj1['attr']; - } - // check type after conditional block - obj1; - - declare const obj2: {} | undefined; - if (isObject1(obj2)) { - obj2; - obj2['attr']; - } - // check type after conditional block - obj2; - - declare function isObject2(value: unknown): value is {}; - - declare const obj3: Record; - if (isObject2(obj3)) { - obj3; - obj3['attr']; - ~~~~~~~~~~~~ -!!! error TS7053: Element implicitly has an 'any' type because expression of type '"attr"' can't be used to index type '{}'. -!!! error TS7053: Property 'attr' does not exist on type '{}'. - } - // check type after conditional block - obj3; - - declare const obj4: Record | undefined; - if (isObject2(obj4)) { - obj4; - obj4['attr']; - ~~~~~~~~~~~~ -!!! error TS7053: Element implicitly has an 'any' type because expression of type '"attr"' can't be used to index type '{}'. -!!! error TS7053: Property 'attr' does not exist on type '{}'. - } - // check type after conditional block - obj4; - \ No newline at end of file diff --git a/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.types b/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.types index f79e73f03e4d6..ef09dea6f7795 100644 --- a/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.types +++ b/tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.types @@ -58,11 +58,11 @@ if (isObject2(obj3)) { >obj3 : Record obj3; ->obj3 : {} +>obj3 : Record obj3['attr']; ->obj3['attr'] : any ->obj3 : {} +>obj3['attr'] : unknown +>obj3 : Record >'attr' : "attr" } // check type after conditional block @@ -78,11 +78,11 @@ if (isObject2(obj4)) { >obj4 : Record | undefined obj4; ->obj4 : {} +>obj4 : Record obj4['attr']; ->obj4['attr'] : any ->obj4 : {} +>obj4['attr'] : unknown +>obj4 : Record >'attr' : "attr" } // check type after conditional block diff --git a/tests/baselines/reference/narrowingMutualSubtypes.types b/tests/baselines/reference/narrowingMutualSubtypes.types index 7cf817697aafb..7ed2f5955d678 100644 --- a/tests/baselines/reference/narrowingMutualSubtypes.types +++ b/tests/baselines/reference/narrowingMutualSubtypes.types @@ -123,11 +123,11 @@ function gg2(x: Record) { >x : Record x; // {} ->x : {} +>x : Record } else { x; // Record ->x : Record +>x : never } x; // Record >x : Record diff --git a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt new file mode 100644 index 0000000000000..0bc84d9f114c7 --- /dev/null +++ b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt @@ -0,0 +1,11 @@ +tests/cases/compiler/typeGuardNarrowByMutableUntypedField.ts(4,9): error TS2322: Type 'unknown' is not assignable to type 'number'. + + +==== tests/cases/compiler/typeGuardNarrowByMutableUntypedField.ts (1 errors) ==== + declare function hasOwnProperty

(target: {}, property: P): target is { [K in P]: unknown }; + declare const arrayLikeOrIterable: ArrayLike | Iterable; + if (hasOwnProperty(arrayLikeOrIterable, 'length')) { + let x: number = arrayLikeOrIterable.length; + ~ +!!! error TS2322: Type 'unknown' is not assignable to type 'number'. + } \ No newline at end of file diff --git a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types index 46231c8ff6b44..767ecf5f7959f 100644 --- a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types +++ b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types @@ -15,7 +15,7 @@ if (hasOwnProperty(arrayLikeOrIterable, 'length')) { let x: number = arrayLikeOrIterable.length; >x : number ->arrayLikeOrIterable.length : number ->arrayLikeOrIterable : ArrayLike ->length : number +>arrayLikeOrIterable.length : unknown +>arrayLikeOrIterable : (ArrayLike | Iterable) & { length: unknown; } +>length : unknown } From 3df807f26c951a17fd9b4261479b3c84908284c7 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 26 Feb 2023 13:38:07 -1000 Subject: [PATCH 3/7] First check for strict subtypes, then check for regular subtypes --- src/compiler/checker.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0db9c3565beae..2bea34aa8b19e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -27254,7 +27254,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // We first attempt to filter the current type, narrowing constituents as appropriate and removing // constituents that are unrelated to the candidate. - const isRelated = checkDerived ? isTypeDerivedFrom : isTypeStrictSubtypeOf; + const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf; const keyPropertyName = type.flags & TypeFlags.Union ? getKeyPropertyName(type as UnionType) : undefined; const narrowedType = mapType(candidate, c => { // If a discriminant property is available, use that to reduce the type. @@ -27264,7 +27264,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // specific of the two. When t and c are related in both directions, we prefer c for type predicates // because that is the asserted type, but t for `instanceof` because generics aren't reflected in // prototype object types. - const directlyRelated = mapType(matching || type, t => isRelated(t, c) ? t : isRelated(c, t) ? c : neverType); + const directlyRelated = mapType(matching || type, checkDerived ? + t => isTypeDerivedFrom(t, c) ? t : isTypeDerivedFrom(c, t) ? c : neverType : + t => isTypeStrictSubtypeOf(t, c) ? t : isTypeStrictSubtypeOf(c, t) ? c : isTypeSubtypeOf(t, c) ? t : isTypeSubtypeOf(c, t) ? c : neverType); // If no constituents are directly related, create intersections for any generic constituents that // are related by constraint. return directlyRelated.flags & TypeFlags.Never ? @@ -27274,7 +27276,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // If filtering produced a non-empty type, return that. Otherwise, pick the most specific of the two // based on assignability, or as a last resort produce an intersection. return !(narrowedType.flags & TypeFlags.Never) ? narrowedType : - isTypeStrictSubtypeOf(candidate, type) ? candidate : + isTypeSubtypeOf(candidate, type) ? candidate : isTypeAssignableTo(type, candidate) ? type : isTypeAssignableTo(candidate, type) ? candidate : getIntersectionType([type, candidate]); From d737eeea277452b9fd6f037083c35f0dfe965503 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 26 Feb 2023 13:38:30 -1000 Subject: [PATCH 4/7] Accept new baselines --- .../typeGuardNarrowByMutableUntypedField.errors.txt | 11 ----------- .../typeGuardNarrowByMutableUntypedField.types | 6 +++--- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt diff --git a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt deleted file mode 100644 index 0bc84d9f114c7..0000000000000 --- a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.errors.txt +++ /dev/null @@ -1,11 +0,0 @@ -tests/cases/compiler/typeGuardNarrowByMutableUntypedField.ts(4,9): error TS2322: Type 'unknown' is not assignable to type 'number'. - - -==== tests/cases/compiler/typeGuardNarrowByMutableUntypedField.ts (1 errors) ==== - declare function hasOwnProperty

(target: {}, property: P): target is { [K in P]: unknown }; - declare const arrayLikeOrIterable: ArrayLike | Iterable; - if (hasOwnProperty(arrayLikeOrIterable, 'length')) { - let x: number = arrayLikeOrIterable.length; - ~ -!!! error TS2322: Type 'unknown' is not assignable to type 'number'. - } \ No newline at end of file diff --git a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types index 767ecf5f7959f..46231c8ff6b44 100644 --- a/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types +++ b/tests/baselines/reference/typeGuardNarrowByMutableUntypedField.types @@ -15,7 +15,7 @@ if (hasOwnProperty(arrayLikeOrIterable, 'length')) { let x: number = arrayLikeOrIterable.length; >x : number ->arrayLikeOrIterable.length : unknown ->arrayLikeOrIterable : (ArrayLike | Iterable) & { length: unknown; } ->length : unknown +>arrayLikeOrIterable.length : number +>arrayLikeOrIterable : ArrayLike +>length : number } From 9b2d6026e0e909f111acf7b4087567eba595ef4b Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 26 Feb 2023 18:21:27 -1000 Subject: [PATCH 5/7] Add tests --- .../strictSubtypeAndNarrowing.errors.txt | 31 ++++++++ .../reference/strictSubtypeAndNarrowing.js | 58 ++++++++++++++ .../strictSubtypeAndNarrowing.symbols | 73 ++++++++++++++++++ .../reference/strictSubtypeAndNarrowing.types | 76 +++++++++++++++++++ .../compiler/strictSubtypeAndNarrowing.ts | 31 ++++++++ 5 files changed, 269 insertions(+) diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt index 1dd63fe6e0f77..db2bff42310ec 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt @@ -146,4 +146,35 @@ tests/cases/compiler/strictSubtypeAndNarrowing.ts(129,26): error TS2322: Type '{ !!! error TS2322: Type '{ x: number; y: number; }' is not assignable to type '{ x?: number | undefined; }'. !!! error TS2322: Object literal may only specify known properties, and 'y' does not exist in type '{ x?: number | undefined; }'. } + + // Repros from #52827 + + declare function isArrayLike(value: any): value is { length: number }; + + function ff1(value: { [index: number]: boolean, length: number } | undefined) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; + } + + function ff2(value: { [index: number]: boolean, length: number } | string) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; + } + + function ff3(value: string | string[] | { [index: number]: boolean, length: number } | [number, boolean] | number | { length: string } | { a: string } | null | undefined) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; + } \ No newline at end of file diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.js b/tests/baselines/reference/strictSubtypeAndNarrowing.js index 3e135c3af6fc1..c903dc872097c 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.js +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.js @@ -129,6 +129,37 @@ function fx11(): { x?: number } { let obj: { x?: number, y?: number }; return obj = { x: 1, y: 2 }; } + +// Repros from #52827 + +declare function isArrayLike(value: any): value is { length: number }; + +function ff1(value: { [index: number]: boolean, length: number } | undefined) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; +} + +function ff2(value: { [index: number]: boolean, length: number } | string) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; +} + +function ff3(value: string | string[] | { [index: number]: boolean, length: number } | [number, boolean] | number | { length: string } | { a: string } | null | undefined) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; +} //// [strictSubtypeAndNarrowing.js] @@ -226,3 +257,30 @@ function fx11() { var obj; return obj = { x: 1, y: 2 }; } +function ff1(value) { + if (isArrayLike(value)) { + value; + } + else { + value; + } + value; +} +function ff2(value) { + if (isArrayLike(value)) { + value; + } + else { + value; + } + value; +} +function ff3(value) { + if (isArrayLike(value)) { + value; + } + else { + value; + } + value; +} diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols index 2bdf3b7777cae..cd717df248725 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols @@ -311,3 +311,76 @@ function fx11(): { x?: number } { >y : Symbol(y, Decl(strictSubtypeAndNarrowing.ts, 128, 24)) } +// Repros from #52827 + +declare function isArrayLike(value: any): value is { length: number }; +>isArrayLike : Symbol(isArrayLike, Decl(strictSubtypeAndNarrowing.ts, 129, 1)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 133, 29)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 133, 29)) +>length : Symbol(length, Decl(strictSubtypeAndNarrowing.ts, 133, 52)) + +function ff1(value: { [index: number]: boolean, length: number } | undefined) { +>ff1 : Symbol(ff1, Decl(strictSubtypeAndNarrowing.ts, 133, 70)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 135, 13)) +>index : Symbol(index, Decl(strictSubtypeAndNarrowing.ts, 135, 23)) +>length : Symbol(length, Decl(strictSubtypeAndNarrowing.ts, 135, 47)) + + if (isArrayLike(value)) { +>isArrayLike : Symbol(isArrayLike, Decl(strictSubtypeAndNarrowing.ts, 129, 1)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 135, 13)) + + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 135, 13)) + + } else { + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 135, 13)) + } + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 135, 13)) +} + +function ff2(value: { [index: number]: boolean, length: number } | string) { +>ff2 : Symbol(ff2, Decl(strictSubtypeAndNarrowing.ts, 142, 1)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 144, 13)) +>index : Symbol(index, Decl(strictSubtypeAndNarrowing.ts, 144, 23)) +>length : Symbol(length, Decl(strictSubtypeAndNarrowing.ts, 144, 47)) + + if (isArrayLike(value)) { +>isArrayLike : Symbol(isArrayLike, Decl(strictSubtypeAndNarrowing.ts, 129, 1)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 144, 13)) + + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 144, 13)) + + } else { + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 144, 13)) + } + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 144, 13)) +} + +function ff3(value: string | string[] | { [index: number]: boolean, length: number } | [number, boolean] | number | { length: string } | { a: string } | null | undefined) { +>ff3 : Symbol(ff3, Decl(strictSubtypeAndNarrowing.ts, 151, 1)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 153, 13)) +>index : Symbol(index, Decl(strictSubtypeAndNarrowing.ts, 153, 43)) +>length : Symbol(length, Decl(strictSubtypeAndNarrowing.ts, 153, 67)) +>length : Symbol(length, Decl(strictSubtypeAndNarrowing.ts, 153, 117)) +>a : Symbol(a, Decl(strictSubtypeAndNarrowing.ts, 153, 138)) + + if (isArrayLike(value)) { +>isArrayLike : Symbol(isArrayLike, Decl(strictSubtypeAndNarrowing.ts, 129, 1)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 153, 13)) + + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 153, 13)) + + } else { + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 153, 13)) + } + value; +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 153, 13)) +} + diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.types b/tests/baselines/reference/strictSubtypeAndNarrowing.types index 8f2e6a2cd8c83..e87900a19bb24 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.types +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.types @@ -326,3 +326,79 @@ function fx11(): { x?: number } { >2 : 2 } +// Repros from #52827 + +declare function isArrayLike(value: any): value is { length: number }; +>isArrayLike : (value: any) => value is { length: number; } +>value : any +>length : number + +function ff1(value: { [index: number]: boolean, length: number } | undefined) { +>ff1 : (value: { [index: number]: boolean; length: number; } | undefined) => void +>value : { [index: number]: boolean; length: number; } | undefined +>index : number +>length : number + + if (isArrayLike(value)) { +>isArrayLike(value) : boolean +>isArrayLike : (value: any) => value is { length: number; } +>value : { [index: number]: boolean; length: number; } | undefined + + value; +>value : { [index: number]: boolean; length: number; } + + } else { + value; +>value : undefined + } + value; +>value : { [index: number]: boolean; length: number; } | undefined +} + +function ff2(value: { [index: number]: boolean, length: number } | string) { +>ff2 : (value: string | { [index: number]: boolean; length: number; }) => void +>value : string | { [index: number]: boolean; length: number; } +>index : number +>length : number + + if (isArrayLike(value)) { +>isArrayLike(value) : boolean +>isArrayLike : (value: any) => value is { length: number; } +>value : string | { [index: number]: boolean; length: number; } + + value; +>value : string | { [index: number]: boolean; length: number; } + + } else { + value; +>value : never + } + value; +>value : string | { [index: number]: boolean; length: number; } +} + +function ff3(value: string | string[] | { [index: number]: boolean, length: number } | [number, boolean] | number | { length: string } | { a: string } | null | undefined) { +>ff3 : (value: string | number | { [index: number]: boolean; length: number; } | [number, boolean] | { length: string; } | { a: string; } | string[] | null | undefined) => void +>value : string | number | { [index: number]: boolean; length: number; } | [number, boolean] | { length: string; } | { a: string; } | string[] | null | undefined +>index : number +>length : number +>length : string +>a : string +>null : null + + if (isArrayLike(value)) { +>isArrayLike(value) : boolean +>isArrayLike : (value: any) => value is { length: number; } +>value : string | number | { [index: number]: boolean; length: number; } | [number, boolean] | { length: string; } | { a: string; } | string[] | null | undefined + + value; +>value : string | { [index: number]: boolean; length: number; } | [number, boolean] | string[] + + } else { + value; +>value : number | { length: string; } | { a: string; } | null | undefined + } + value; +>value : string | number | { [index: number]: boolean; length: number; } | [number, boolean] | { length: string; } | { a: string; } | string[] | null | undefined +} + diff --git a/tests/cases/compiler/strictSubtypeAndNarrowing.ts b/tests/cases/compiler/strictSubtypeAndNarrowing.ts index 6239072f97efe..de273973a748b 100644 --- a/tests/cases/compiler/strictSubtypeAndNarrowing.ts +++ b/tests/cases/compiler/strictSubtypeAndNarrowing.ts @@ -130,3 +130,34 @@ function fx11(): { x?: number } { let obj: { x?: number, y?: number }; return obj = { x: 1, y: 2 }; } + +// Repros from #52827 + +declare function isArrayLike(value: any): value is { length: number }; + +function ff1(value: { [index: number]: boolean, length: number } | undefined) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; +} + +function ff2(value: { [index: number]: boolean, length: number } | string) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; +} + +function ff3(value: string | string[] | { [index: number]: boolean, length: number } | [number, boolean] | number | { length: string } | { a: string } | null | undefined) { + if (isArrayLike(value)) { + value; + } else { + value; + } + value; +} From 9ea8a55577a9455824ca3f36898338906cb6bb45 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 3 Mar 2023 10:38:01 -0800 Subject: [PATCH 6/7] Accept new baselines --- .../strictSubtypeAndNarrowing.errors.txt | 49 +++++ .../reference/strictSubtypeAndNarrowing.js | 57 ++++++ .../strictSubtypeAndNarrowing.symbols | 172 ++++++++++++++++++ .../reference/strictSubtypeAndNarrowing.types | 112 ++++++++++++ .../compiler/strictSubtypeAndNarrowing.ts | 49 +++++ 5 files changed, 439 insertions(+) diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt index db2bff42310ec..34fa818ba5323 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt @@ -177,4 +177,53 @@ tests/cases/compiler/strictSubtypeAndNarrowing.ts(129,26): error TS2322: Type '{ } value; } + + // Repro from comment in #52984 + + type DistributedKeyOf = T extends unknown ? keyof T : never; + + type NarrowByKeyValue = ObjT extends unknown + ? KeyT extends keyof ObjT + ? ValueT extends ObjT[KeyT] + ? ObjT & Readonly> + : never + : never + : never; + + type NarrowByDeepValue = DeepPathT extends readonly [ + infer Head extends DistributedKeyOf, + ] + ? NarrowByKeyValue + : DeepPathT extends readonly [infer Head extends DistributedKeyOf, ...infer Rest] + ? NarrowByKeyValue, Rest, ValueT>> + : never; + + + declare function doesValueAtDeepPathSatisfy< + ObjT extends object, + const DeepPathT extends ReadonlyArray, + ValueT, + >( + obj: ObjT, + deepPath: DeepPathT, + predicate: (arg: unknown) => arg is ValueT, + ): obj is NarrowByDeepValue; + + + type Foo = {value: {type: 'A'}; a?: number} | {value: {type: 'B'}; b?: number}; + + declare function isA(arg: unknown): arg is 'A'; + declare function isB(arg: unknown): arg is 'B'; + + declare function assert(condition: boolean): asserts condition; + + function test1(foo: Foo): {value: {type: 'A'}; a?: number} { + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); + return foo; + } + + function test2(foo: Foo): {value: {type: 'A'}; a?: number} { + assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); + return foo; + } \ No newline at end of file diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.js b/tests/baselines/reference/strictSubtypeAndNarrowing.js index c903dc872097c..0a1b3d87979a9 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.js +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.js @@ -160,6 +160,55 @@ function ff3(value: string | string[] | { [index: number]: boolean, length: numb } value; } + +// Repro from comment in #52984 + +type DistributedKeyOf = T extends unknown ? keyof T : never; + +type NarrowByKeyValue = ObjT extends unknown + ? KeyT extends keyof ObjT + ? ValueT extends ObjT[KeyT] + ? ObjT & Readonly> + : never + : never + : never; + +type NarrowByDeepValue = DeepPathT extends readonly [ + infer Head extends DistributedKeyOf, +] + ? NarrowByKeyValue + : DeepPathT extends readonly [infer Head extends DistributedKeyOf, ...infer Rest] + ? NarrowByKeyValue, Rest, ValueT>> + : never; + + +declare function doesValueAtDeepPathSatisfy< + ObjT extends object, + const DeepPathT extends ReadonlyArray, + ValueT, +>( + obj: ObjT, + deepPath: DeepPathT, + predicate: (arg: unknown) => arg is ValueT, +): obj is NarrowByDeepValue; + + +type Foo = {value: {type: 'A'}; a?: number} | {value: {type: 'B'}; b?: number}; + +declare function isA(arg: unknown): arg is 'A'; +declare function isB(arg: unknown): arg is 'B'; + +declare function assert(condition: boolean): asserts condition; + +function test1(foo: Foo): {value: {type: 'A'}; a?: number} { + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); + return foo; +} + +function test2(foo: Foo): {value: {type: 'A'}; a?: number} { + assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); + return foo; +} //// [strictSubtypeAndNarrowing.js] @@ -284,3 +333,11 @@ function ff3(value) { } value; } +function test1(foo) { + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); + return foo; +} +function test2(foo) { + assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); + return foo; +} diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols index cd717df248725..3758fba25ea4e 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols @@ -384,3 +384,175 @@ function ff3(value: string | string[] | { [index: number]: boolean, length: numb >value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 153, 13)) } +// Repro from comment in #52984 + +type DistributedKeyOf = T extends unknown ? keyof T : never; +>DistributedKeyOf : Symbol(DistributedKeyOf, Decl(strictSubtypeAndNarrowing.ts, 160, 1)) +>T : Symbol(T, Decl(strictSubtypeAndNarrowing.ts, 164, 22)) +>T : Symbol(T, Decl(strictSubtypeAndNarrowing.ts, 164, 22)) +>T : Symbol(T, Decl(strictSubtypeAndNarrowing.ts, 164, 22)) + +type NarrowByKeyValue = ObjT extends unknown +>NarrowByKeyValue : Symbol(NarrowByKeyValue, Decl(strictSubtypeAndNarrowing.ts, 164, 63)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 166, 22)) +>KeyT : Symbol(KeyT, Decl(strictSubtypeAndNarrowing.ts, 166, 27)) +>PropertyKey : Symbol(PropertyKey, Decl(lib.es5.d.ts, --, --)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 166, 53)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 166, 22)) + + ? KeyT extends keyof ObjT +>KeyT : Symbol(KeyT, Decl(strictSubtypeAndNarrowing.ts, 166, 27)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 166, 22)) + + ? ValueT extends ObjT[KeyT] +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 166, 53)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 166, 22)) +>KeyT : Symbol(KeyT, Decl(strictSubtypeAndNarrowing.ts, 166, 27)) + + ? ObjT & Readonly> +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 166, 22)) +>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>KeyT : Symbol(KeyT, Decl(strictSubtypeAndNarrowing.ts, 166, 27)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 166, 53)) + + : never + : never + : never; + +type NarrowByDeepValue = DeepPathT extends readonly [ +>NarrowByDeepValue : Symbol(NarrowByDeepValue, Decl(strictSubtypeAndNarrowing.ts, 172, 12)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 174, 23)) +>DeepPathT : Symbol(DeepPathT, Decl(strictSubtypeAndNarrowing.ts, 174, 28)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 174, 39)) +>DeepPathT : Symbol(DeepPathT, Decl(strictSubtypeAndNarrowing.ts, 174, 28)) + + infer Head extends DistributedKeyOf, +>Head : Symbol(Head, Decl(strictSubtypeAndNarrowing.ts, 175, 9)) +>DistributedKeyOf : Symbol(DistributedKeyOf, Decl(strictSubtypeAndNarrowing.ts, 160, 1)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 174, 23)) + +] + ? NarrowByKeyValue +>NarrowByKeyValue : Symbol(NarrowByKeyValue, Decl(strictSubtypeAndNarrowing.ts, 164, 63)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 174, 23)) +>Head : Symbol(Head, Decl(strictSubtypeAndNarrowing.ts, 175, 9)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 174, 39)) + + : DeepPathT extends readonly [infer Head extends DistributedKeyOf, ...infer Rest] +>DeepPathT : Symbol(DeepPathT, Decl(strictSubtypeAndNarrowing.ts, 174, 28)) +>Head : Symbol(Head, Decl(strictSubtypeAndNarrowing.ts, 178, 39)) +>DistributedKeyOf : Symbol(DistributedKeyOf, Decl(strictSubtypeAndNarrowing.ts, 160, 1)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 174, 23)) +>Rest : Symbol(Rest, Decl(strictSubtypeAndNarrowing.ts, 178, 85)) + + ? NarrowByKeyValue, Rest, ValueT>> +>NarrowByKeyValue : Symbol(NarrowByKeyValue, Decl(strictSubtypeAndNarrowing.ts, 164, 63)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 174, 23)) +>Head : Symbol(Head, Decl(strictSubtypeAndNarrowing.ts, 178, 39)) +>NarrowByDeepValue : Symbol(NarrowByDeepValue, Decl(strictSubtypeAndNarrowing.ts, 172, 12)) +>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 174, 23)) +>Head : Symbol(Head, Decl(strictSubtypeAndNarrowing.ts, 178, 39)) +>Rest : Symbol(Rest, Decl(strictSubtypeAndNarrowing.ts, 178, 85)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 174, 39)) + + : never; + + +declare function doesValueAtDeepPathSatisfy< +>doesValueAtDeepPathSatisfy : Symbol(doesValueAtDeepPathSatisfy, Decl(strictSubtypeAndNarrowing.ts, 180, 12)) + + ObjT extends object, +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 183, 44)) + + const DeepPathT extends ReadonlyArray, +>DeepPathT : Symbol(DeepPathT, Decl(strictSubtypeAndNarrowing.ts, 184, 24)) +>ReadonlyArray : Symbol(ReadonlyArray, Decl(lib.es5.d.ts, --, --)) + + ValueT, +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 185, 59)) + +>( + obj: ObjT, +>obj : Symbol(obj, Decl(strictSubtypeAndNarrowing.ts, 187, 2)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 183, 44)) + + deepPath: DeepPathT, +>deepPath : Symbol(deepPath, Decl(strictSubtypeAndNarrowing.ts, 188, 14)) +>DeepPathT : Symbol(DeepPathT, Decl(strictSubtypeAndNarrowing.ts, 184, 24)) + + predicate: (arg: unknown) => arg is ValueT, +>predicate : Symbol(predicate, Decl(strictSubtypeAndNarrowing.ts, 189, 24)) +>arg : Symbol(arg, Decl(strictSubtypeAndNarrowing.ts, 190, 16)) +>arg : Symbol(arg, Decl(strictSubtypeAndNarrowing.ts, 190, 16)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 185, 59)) + +): obj is NarrowByDeepValue; +>obj : Symbol(obj, Decl(strictSubtypeAndNarrowing.ts, 187, 2)) +>NarrowByDeepValue : Symbol(NarrowByDeepValue, Decl(strictSubtypeAndNarrowing.ts, 172, 12)) +>ObjT : Symbol(ObjT, Decl(strictSubtypeAndNarrowing.ts, 183, 44)) +>DeepPathT : Symbol(DeepPathT, Decl(strictSubtypeAndNarrowing.ts, 184, 24)) +>ValueT : Symbol(ValueT, Decl(strictSubtypeAndNarrowing.ts, 185, 59)) + + +type Foo = {value: {type: 'A'}; a?: number} | {value: {type: 'B'}; b?: number}; +>Foo : Symbol(Foo, Decl(strictSubtypeAndNarrowing.ts, 191, 53)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 194, 12)) +>type : Symbol(type, Decl(strictSubtypeAndNarrowing.ts, 194, 20)) +>a : Symbol(a, Decl(strictSubtypeAndNarrowing.ts, 194, 31)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 194, 47)) +>type : Symbol(type, Decl(strictSubtypeAndNarrowing.ts, 194, 55)) +>b : Symbol(b, Decl(strictSubtypeAndNarrowing.ts, 194, 66)) + +declare function isA(arg: unknown): arg is 'A'; +>isA : Symbol(isA, Decl(strictSubtypeAndNarrowing.ts, 194, 79)) +>arg : Symbol(arg, Decl(strictSubtypeAndNarrowing.ts, 196, 21)) +>arg : Symbol(arg, Decl(strictSubtypeAndNarrowing.ts, 196, 21)) + +declare function isB(arg: unknown): arg is 'B'; +>isB : Symbol(isB, Decl(strictSubtypeAndNarrowing.ts, 196, 47)) +>arg : Symbol(arg, Decl(strictSubtypeAndNarrowing.ts, 197, 21)) +>arg : Symbol(arg, Decl(strictSubtypeAndNarrowing.ts, 197, 21)) + +declare function assert(condition: boolean): asserts condition; +>assert : Symbol(assert, Decl(strictSubtypeAndNarrowing.ts, 197, 47)) +>condition : Symbol(condition, Decl(strictSubtypeAndNarrowing.ts, 199, 24)) +>condition : Symbol(condition, Decl(strictSubtypeAndNarrowing.ts, 199, 24)) + +function test1(foo: Foo): {value: {type: 'A'}; a?: number} { +>test1 : Symbol(test1, Decl(strictSubtypeAndNarrowing.ts, 199, 63)) +>foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 201, 15)) +>Foo : Symbol(Foo, Decl(strictSubtypeAndNarrowing.ts, 191, 53)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 201, 27)) +>type : Symbol(type, Decl(strictSubtypeAndNarrowing.ts, 201, 35)) +>a : Symbol(a, Decl(strictSubtypeAndNarrowing.ts, 201, 46)) + + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); +>assert : Symbol(assert, Decl(strictSubtypeAndNarrowing.ts, 197, 47)) +>doesValueAtDeepPathSatisfy : Symbol(doesValueAtDeepPathSatisfy, Decl(strictSubtypeAndNarrowing.ts, 180, 12)) +>foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 201, 15)) +>isA : Symbol(isA, Decl(strictSubtypeAndNarrowing.ts, 194, 79)) + + return foo; +>foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 201, 15)) +} + +function test2(foo: Foo): {value: {type: 'A'}; a?: number} { +>test2 : Symbol(test2, Decl(strictSubtypeAndNarrowing.ts, 204, 1)) +>foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 206, 15)) +>Foo : Symbol(Foo, Decl(strictSubtypeAndNarrowing.ts, 191, 53)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 206, 27)) +>type : Symbol(type, Decl(strictSubtypeAndNarrowing.ts, 206, 35)) +>a : Symbol(a, Decl(strictSubtypeAndNarrowing.ts, 206, 46)) + + assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); +>assert : Symbol(assert, Decl(strictSubtypeAndNarrowing.ts, 197, 47)) +>doesValueAtDeepPathSatisfy : Symbol(doesValueAtDeepPathSatisfy, Decl(strictSubtypeAndNarrowing.ts, 180, 12)) +>foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 206, 15)) +>isB : Symbol(isB, Decl(strictSubtypeAndNarrowing.ts, 196, 47)) + + return foo; +>foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 206, 15)) +} + diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.types b/tests/baselines/reference/strictSubtypeAndNarrowing.types index e87900a19bb24..41e7f12b1861a 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.types +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.types @@ -402,3 +402,115 @@ function ff3(value: string | string[] | { [index: number]: boolean, length: numb >value : string | number | { [index: number]: boolean; length: number; } | [number, boolean] | { length: string; } | { a: string; } | string[] | null | undefined } +// Repro from comment in #52984 + +type DistributedKeyOf = T extends unknown ? keyof T : never; +>DistributedKeyOf : DistributedKeyOf + +type NarrowByKeyValue = ObjT extends unknown +>NarrowByKeyValue : NarrowByKeyValue + + ? KeyT extends keyof ObjT + ? ValueT extends ObjT[KeyT] + ? ObjT & Readonly> + : never + : never + : never; + +type NarrowByDeepValue = DeepPathT extends readonly [ +>NarrowByDeepValue : NarrowByDeepValue + + infer Head extends DistributedKeyOf, +] + ? NarrowByKeyValue + : DeepPathT extends readonly [infer Head extends DistributedKeyOf, ...infer Rest] + ? NarrowByKeyValue, Rest, ValueT>> + : never; + + +declare function doesValueAtDeepPathSatisfy< +>doesValueAtDeepPathSatisfy : (obj: ObjT, deepPath: DeepPathT, predicate: (arg: unknown) => arg is ValueT) => obj is NarrowByDeepValue + + ObjT extends object, + const DeepPathT extends ReadonlyArray, + ValueT, +>( + obj: ObjT, +>obj : ObjT + + deepPath: DeepPathT, +>deepPath : DeepPathT + + predicate: (arg: unknown) => arg is ValueT, +>predicate : (arg: unknown) => arg is ValueT +>arg : unknown + +): obj is NarrowByDeepValue; + + +type Foo = {value: {type: 'A'}; a?: number} | {value: {type: 'B'}; b?: number}; +>Foo : { value: { type: 'A';}; a?: number | undefined; } | { value: { type: 'B';}; b?: number | undefined; } +>value : { type: 'A'; } +>type : "A" +>a : number | undefined +>value : { type: 'B'; } +>type : "B" +>b : number | undefined + +declare function isA(arg: unknown): arg is 'A'; +>isA : (arg: unknown) => arg is "A" +>arg : unknown + +declare function isB(arg: unknown): arg is 'B'; +>isB : (arg: unknown) => arg is "B" +>arg : unknown + +declare function assert(condition: boolean): asserts condition; +>assert : (condition: boolean) => asserts condition +>condition : boolean + +function test1(foo: Foo): {value: {type: 'A'}; a?: number} { +>test1 : (foo: Foo) => { value: { type: 'A'; }; a?: number;} +>foo : Foo +>value : { type: 'A'; } +>type : "A" +>a : number | undefined + + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); +>assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)) : void +>assert : (condition: boolean) => asserts condition +>doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA) : boolean +>doesValueAtDeepPathSatisfy : (obj: ObjT, deepPath: DeepPathT, predicate: (arg: unknown) => arg is ValueT) => obj is NarrowByDeepValue +>foo : Foo +>['value', 'type'] : ["value", "type"] +>'value' : "value" +>'type' : "type" +>isA : (arg: unknown) => arg is "A" + + return foo; +>foo : { value: { type: "A"; }; a?: number | undefined; } +} + +function test2(foo: Foo): {value: {type: 'A'}; a?: number} { +>test2 : (foo: Foo) => { value: { type: 'A'; }; a?: number;} +>foo : Foo +>value : { type: 'A'; } +>type : "A" +>a : number | undefined + + assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); +>assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)) : void +>assert : (condition: boolean) => asserts condition +>!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB) : boolean +>doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB) : boolean +>doesValueAtDeepPathSatisfy : (obj: ObjT, deepPath: DeepPathT, predicate: (arg: unknown) => arg is ValueT) => obj is NarrowByDeepValue +>foo : Foo +>['value', 'type'] : ["value", "type"] +>'value' : "value" +>'type' : "type" +>isB : (arg: unknown) => arg is "B" + + return foo; +>foo : { value: { type: "A"; }; a?: number | undefined; } +} + diff --git a/tests/cases/compiler/strictSubtypeAndNarrowing.ts b/tests/cases/compiler/strictSubtypeAndNarrowing.ts index de273973a748b..1f00efc0ec730 100644 --- a/tests/cases/compiler/strictSubtypeAndNarrowing.ts +++ b/tests/cases/compiler/strictSubtypeAndNarrowing.ts @@ -161,3 +161,52 @@ function ff3(value: string | string[] | { [index: number]: boolean, length: numb } value; } + +// Repro from comment in #52984 + +type DistributedKeyOf = T extends unknown ? keyof T : never; + +type NarrowByKeyValue = ObjT extends unknown + ? KeyT extends keyof ObjT + ? ValueT extends ObjT[KeyT] + ? ObjT & Readonly> + : never + : never + : never; + +type NarrowByDeepValue = DeepPathT extends readonly [ + infer Head extends DistributedKeyOf, +] + ? NarrowByKeyValue + : DeepPathT extends readonly [infer Head extends DistributedKeyOf, ...infer Rest] + ? NarrowByKeyValue, Rest, ValueT>> + : never; + + +declare function doesValueAtDeepPathSatisfy< + ObjT extends object, + const DeepPathT extends ReadonlyArray, + ValueT, +>( + obj: ObjT, + deepPath: DeepPathT, + predicate: (arg: unknown) => arg is ValueT, +): obj is NarrowByDeepValue; + + +type Foo = {value: {type: 'A'}; a?: number} | {value: {type: 'B'}; b?: number}; + +declare function isA(arg: unknown): arg is 'A'; +declare function isB(arg: unknown): arg is 'B'; + +declare function assert(condition: boolean): asserts condition; + +function test1(foo: Foo): {value: {type: 'A'}; a?: number} { + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); + return foo; +} + +function test2(foo: Foo): {value: {type: 'A'}; a?: number} { + assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); + return foo; +} From 8bb30e269c469fa52b0b11fbfa71b1da95388383 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Fri, 3 Mar 2023 15:51:41 -0800 Subject: [PATCH 7/7] Add another repro --- .../strictSubtypeAndNarrowing.errors.txt | 20 +++++++++ .../reference/strictSubtypeAndNarrowing.js | 25 +++++++++++ .../strictSubtypeAndNarrowing.symbols | 45 +++++++++++++++++++ .../reference/strictSubtypeAndNarrowing.types | 43 ++++++++++++++++++ .../compiler/strictSubtypeAndNarrowing.ts | 20 +++++++++ 5 files changed, 153 insertions(+) diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt index 34fa818ba5323..12d23ddb7b5b7 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt @@ -226,4 +226,24 @@ tests/cases/compiler/strictSubtypeAndNarrowing.ts(129,26): error TS2322: Type '{ assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); return foo; } + + // Repro from #53063 + + interface Free { + premium: false; + } + + interface Premium { + premium: true; + } + + type Union = { premium: false } | { premium: true }; + + declare const checkIsPremium: (a: Union) => a is Union & Premium; + + const f = (value: Union) => { + if (!checkIsPremium(value)) { + value.premium; + } + }; \ No newline at end of file diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.js b/tests/baselines/reference/strictSubtypeAndNarrowing.js index 0a1b3d87979a9..ec143633beba9 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.js +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.js @@ -209,6 +209,26 @@ function test2(foo: Foo): {value: {type: 'A'}; a?: number} { assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); return foo; } + +// Repro from #53063 + +interface Free { + premium: false; +} + +interface Premium { + premium: true; +} + +type Union = { premium: false } | { premium: true }; + +declare const checkIsPremium: (a: Union) => a is Union & Premium; + +const f = (value: Union) => { + if (!checkIsPremium(value)) { + value.premium; + } +}; //// [strictSubtypeAndNarrowing.js] @@ -341,3 +361,8 @@ function test2(foo) { assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); return foo; } +var f = function (value) { + if (!checkIsPremium(value)) { + value.premium; + } +}; diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols index 3758fba25ea4e..99dde89722e12 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols @@ -556,3 +556,48 @@ function test2(foo: Foo): {value: {type: 'A'}; a?: number} { >foo : Symbol(foo, Decl(strictSubtypeAndNarrowing.ts, 206, 15)) } +// Repro from #53063 + +interface Free { +>Free : Symbol(Free, Decl(strictSubtypeAndNarrowing.ts, 209, 1)) + + premium: false; +>premium : Symbol(Free.premium, Decl(strictSubtypeAndNarrowing.ts, 213, 16)) +} + +interface Premium { +>Premium : Symbol(Premium, Decl(strictSubtypeAndNarrowing.ts, 215, 1)) + + premium: true; +>premium : Symbol(Premium.premium, Decl(strictSubtypeAndNarrowing.ts, 217, 19)) +} + +type Union = { premium: false } | { premium: true }; +>Union : Symbol(Union, Decl(strictSubtypeAndNarrowing.ts, 219, 1)) +>premium : Symbol(premium, Decl(strictSubtypeAndNarrowing.ts, 221, 14)) +>premium : Symbol(premium, Decl(strictSubtypeAndNarrowing.ts, 221, 35)) + +declare const checkIsPremium: (a: Union) => a is Union & Premium; +>checkIsPremium : Symbol(checkIsPremium, Decl(strictSubtypeAndNarrowing.ts, 223, 13)) +>a : Symbol(a, Decl(strictSubtypeAndNarrowing.ts, 223, 31)) +>Union : Symbol(Union, Decl(strictSubtypeAndNarrowing.ts, 219, 1)) +>a : Symbol(a, Decl(strictSubtypeAndNarrowing.ts, 223, 31)) +>Union : Symbol(Union, Decl(strictSubtypeAndNarrowing.ts, 219, 1)) +>Premium : Symbol(Premium, Decl(strictSubtypeAndNarrowing.ts, 215, 1)) + +const f = (value: Union) => { +>f : Symbol(f, Decl(strictSubtypeAndNarrowing.ts, 225, 5)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 225, 11)) +>Union : Symbol(Union, Decl(strictSubtypeAndNarrowing.ts, 219, 1)) + + if (!checkIsPremium(value)) { +>checkIsPremium : Symbol(checkIsPremium, Decl(strictSubtypeAndNarrowing.ts, 223, 13)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 225, 11)) + + value.premium; +>value.premium : Symbol(premium, Decl(strictSubtypeAndNarrowing.ts, 221, 14)) +>value : Symbol(value, Decl(strictSubtypeAndNarrowing.ts, 225, 11)) +>premium : Symbol(premium, Decl(strictSubtypeAndNarrowing.ts, 221, 14)) + } +}; + diff --git a/tests/baselines/reference/strictSubtypeAndNarrowing.types b/tests/baselines/reference/strictSubtypeAndNarrowing.types index 41e7f12b1861a..1dcee9598a60a 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.types +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.types @@ -514,3 +514,46 @@ function test2(foo: Foo): {value: {type: 'A'}; a?: number} { >foo : { value: { type: "A"; }; a?: number | undefined; } } +// Repro from #53063 + +interface Free { + premium: false; +>premium : false +>false : false +} + +interface Premium { + premium: true; +>premium : true +>true : true +} + +type Union = { premium: false } | { premium: true }; +>Union : { premium: false; } | { premium: true; } +>premium : false +>false : false +>premium : true +>true : true + +declare const checkIsPremium: (a: Union) => a is Union & Premium; +>checkIsPremium : (a: Union) => a is { premium: true; } & Premium +>a : Union + +const f = (value: Union) => { +>f : (value: Union) => void +>(value: Union) => { if (!checkIsPremium(value)) { value.premium; }} : (value: Union) => void +>value : Union + + if (!checkIsPremium(value)) { +>!checkIsPremium(value) : boolean +>checkIsPremium(value) : boolean +>checkIsPremium : (a: Union) => a is { premium: true; } & Premium +>value : Union + + value.premium; +>value.premium : false +>value : { premium: false; } +>premium : false + } +}; + diff --git a/tests/cases/compiler/strictSubtypeAndNarrowing.ts b/tests/cases/compiler/strictSubtypeAndNarrowing.ts index 1f00efc0ec730..58a7629ff4307 100644 --- a/tests/cases/compiler/strictSubtypeAndNarrowing.ts +++ b/tests/cases/compiler/strictSubtypeAndNarrowing.ts @@ -210,3 +210,23 @@ function test2(foo: Foo): {value: {type: 'A'}; a?: number} { assert(!doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isB)); return foo; } + +// Repro from #53063 + +interface Free { + premium: false; +} + +interface Premium { + premium: true; +} + +type Union = { premium: false } | { premium: true }; + +declare const checkIsPremium: (a: Union) => a is Union & Premium; + +const f = (value: Union) => { + if (!checkIsPremium(value)) { + value.premium; + } +};