From b50b20a619747d910679755386942d0f3b4bc75b Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 4 Mar 2023 00:01:25 +0000 Subject: [PATCH] Cherry-pick PR #52984 into release-5.0 Component commits: b3d3ec9944 Use strictSubtypeRelation in getNarrowedType and narrow only for pure subtypes 0325f9dbd9 Accept new baselines 3df807f26c First check for strict subtypes, then check for regular subtypes d737eeea27 Accept new baselines 9b2d6026e0 Add tests 9ea8a55577 Accept new baselines 8bb30e269c Add another repro --- src/compiler/checker.ts | 6 +- ...ssertedTypeThroughTypePredicate.errors.txt | 51 --- ...avorAssertedTypeThroughTypePredicate.types | 12 +- .../reference/narrowingMutualSubtypes.types | 4 +- .../strictSubtypeAndNarrowing.errors.txt | 100 ++++++ .../reference/strictSubtypeAndNarrowing.js | 140 +++++++++ .../strictSubtypeAndNarrowing.symbols | 290 ++++++++++++++++++ .../reference/strictSubtypeAndNarrowing.types | 231 ++++++++++++++ .../compiler/strictSubtypeAndNarrowing.ts | 100 ++++++ 9 files changed, 874 insertions(+), 60 deletions(-) delete mode 100644 tests/baselines/reference/controlFlowFavorAssertedTypeThroughTypePredicate.errors.txt diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0bc1cd61fcfab..5e374efbfba36 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); } @@ -27262,7 +27266,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // 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); + 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 ? 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/strictSubtypeAndNarrowing.errors.txt b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt index 1dd63fe6e0f77..12d23ddb7b5b7 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.errors.txt @@ -146,4 +146,104 @@ 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; + } + + // 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; + } + + // 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 3e135c3af6fc1..ec143633beba9 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.js +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.js @@ -129,6 +129,106 @@ 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; +} + +// 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; +} + +// 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] @@ -226,3 +326,43 @@ 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; +} +function test1(foo) { + assert(doesValueAtDeepPathSatisfy(foo, ['value', 'type'], isA)); + return foo; +} +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 2bdf3b7777cae..99dde89722e12 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.symbols +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.symbols @@ -311,3 +311,293 @@ 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)) +} + +// 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)) +} + +// 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 8f2e6a2cd8c83..1dcee9598a60a 100644 --- a/tests/baselines/reference/strictSubtypeAndNarrowing.types +++ b/tests/baselines/reference/strictSubtypeAndNarrowing.types @@ -326,3 +326,234 @@ 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 +} + +// 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; } +} + +// 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 6239072f97efe..58a7629ff4307 100644 --- a/tests/cases/compiler/strictSubtypeAndNarrowing.ts +++ b/tests/cases/compiler/strictSubtypeAndNarrowing.ts @@ -130,3 +130,103 @@ 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; +} + +// 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; +} + +// 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; + } +};