diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 59fae08cee..e7a2db612e 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -22354,6 +22354,23 @@ func (c *Checker) getObjectTypeInstantiation(t *Type, m *TypeMapper, alias *Type } func (c *Checker) isTypeParameterPossiblyReferenced(tp *Type, node *ast.Node) bool { + var tpDeclaration *ast.Node + if tp.symbol != nil { + declarations := tp.symbol.Declarations + switch { + case len(declarations) == 1: + tpDeclaration = declarations[0] + case tp.AsTypeParameter().isThisType && core.Some(declarations, ast.IsClassDeclaration): + // A class/interface merge gives polymorphic `this` a symbol with multiple + // declarations. Select the declaration that actually contains this node. + for _, declaration := range declarations { + if isNodeDescendantOf(node, declaration) { + tpDeclaration = declaration + break + } + } + } + } var containsReference func(*ast.Node) bool containsReference = func(node *ast.Node) bool { switch node.Kind { @@ -22369,7 +22386,6 @@ func (c *Checker) isTypeParameterPossiblyReferenced(tp *Type, node *ast.Node) bo firstIdentifier := ast.GetFirstIdentifier(entityName) if !ast.IsThisIdentifier(firstIdentifier) { firstIdentifierSymbol := c.getResolvedSymbol(firstIdentifier) - tpDeclaration := tp.symbol.Declarations[0] // There is exactly one declaration, otherwise `containsReference` is not called var tpScope *ast.Node switch { case ast.IsTypeParameterDeclaration(tpDeclaration): @@ -22392,12 +22408,11 @@ func (c *Checker) isTypeParameterPossiblyReferenced(tp *Type, node *ast.Node) bo } return node.ForEachChild(containsReference) } - // If the type parameter doesn't have exactly one declaration, if there are intervening statement blocks - // between the node and the type parameter declaration, if the node contains actual references to the - // type parameter, or if the node contains type queries that we can't prove couldn't contain references to the type parameter, - // we consider the type parameter possibly referenced. - if tp.symbol != nil && len(tp.symbol.Declarations) == 1 { - container := tp.symbol.Declarations[0].Parent + // For regular type parameters with multiple declarations, or a polymorphic `this` whose containing + // declaration can't be identified, conservatively assume a reference. We do the same when there are + // intervening statement blocks, actual references, or type queries whose references can't be disproved. + if tpDeclaration != nil { + container := tpDeclaration.Parent for n := node; n != container; n = n.Parent { if n == nil || ast.IsBlock(n) || ast.IsConditionalTypeNode(n) && containsReference(n.AsConditionalTypeNode().ExtendsType) { return true diff --git a/testdata/baselines/reference/compiler/selfReferenceExactInstancesPreserveFiniteNesting.errors.txt b/testdata/baselines/reference/compiler/selfReferenceExactInstancesPreserveFiniteNesting.errors.txt new file mode 100644 index 0000000000..82db927cb9 --- /dev/null +++ b/testdata/baselines/reference/compiler/selfReferenceExactInstancesPreserveFiniteNesting.errors.txt @@ -0,0 +1,80 @@ +selfReferenceExactInstancesPreserveFiniteNesting.ts(50,5): error TS2322: Type 'Right.Outer' is not assignable to type 'Left.Outer'. + Types of property 'box' are incompatible. + Type 'Box' is not assignable to type 'Box'. + Type 'Right.L2' is not assignable to type 'Left.L2'. + Types of property 'box' are incompatible. + Type 'Box' is not assignable to type 'Box'. + Type 'Right.L3' is not assignable to type 'Left.L3'. + Types of property 'box' are incompatible. + Type 'Box' is not assignable to type 'Box'. + Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. + Types of property 'id' are incompatible. + Type 'number' is not assignable to type 'string'. + + +==== selfReferenceExactInstancesPreserveFiniteNesting.ts (1 errors) ==== + interface Box { + left2(): Box; + left3(): Box; + leftLeaf(): Box; + right2(): Box; + right3(): Box; + rightLeaf(): Box; + value: T; + } + + namespace Left { + export interface Outer { + box: Box; + } + export interface L2 { + box: Box; + } + export interface L3 { + box: Box; + } + export interface Leaf { + id: string; + } + } + + namespace Right { + export interface Outer { + box: Box; + } + export interface L2 { + box: Box; + } + export interface L3 { + box: Box; + } + export interface Leaf { + id: number; + } + } + + declare const box: Box; + box.left2(); + box.left3(); + box.leftLeaf(); + box.right2(); + box.right3(); + box.rightLeaf(); + + function test(left: Left.Outer, right: Right.Outer) { + left = right; + ~~~~ +!!! error TS2322: Type 'Right.Outer' is not assignable to type 'Left.Outer'. +!!! error TS2322: Types of property 'box' are incompatible. +!!! error TS2322: Type 'Box' is not assignable to type 'Box'. +!!! error TS2322: Type 'Right.L2' is not assignable to type 'Left.L2'. +!!! error TS2322: Types of property 'box' are incompatible. +!!! error TS2322: Type 'Box' is not assignable to type 'Box'. +!!! error TS2322: Type 'Right.L3' is not assignable to type 'Left.L3'. +!!! error TS2322: Types of property 'box' are incompatible. +!!! error TS2322: Type 'Box' is not assignable to type 'Box'. +!!! error TS2322: Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. +!!! error TS2322: Types of property 'id' are incompatible. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/selfReferenceInMethodBody.errors.txt b/testdata/baselines/reference/compiler/selfReferenceInMethodBody.errors.txt new file mode 100644 index 0000000000..61bd269c28 --- /dev/null +++ b/testdata/baselines/reference/compiler/selfReferenceInMethodBody.errors.txt @@ -0,0 +1,20 @@ +selfReferenceInMethodBody.ts(5,15): error TS2322: Type 'Box>>' is not assignable to type 'Box>>'. + Type 'Box>' is not assignable to type 'Box>'. + Type 'Box' is not assignable to type 'Box'. + Type 'number' is not assignable to type 'string'. + + +==== selfReferenceInMethodBody.ts (1 errors) ==== + class Box { + value!: T; + + method(source: Box>>) { + const target: Box>> = source; + ~~~~~~ +!!! error TS2322: Type 'Box>>' is not assignable to type 'Box>>'. +!!! error TS2322: Type 'Box>' is not assignable to type 'Box>'. +!!! error TS2322: Type 'Box' is not assignable to type 'Box'. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + } + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/selfReferencePreservesFiniteNesting.errors.txt b/testdata/baselines/reference/compiler/selfReferencePreservesFiniteNesting.errors.txt new file mode 100644 index 0000000000..d5c9edcf41 --- /dev/null +++ b/testdata/baselines/reference/compiler/selfReferencePreservesFiniteNesting.errors.txt @@ -0,0 +1,67 @@ +selfReferencePreservesFiniteNesting.ts(37,5): error TS2322: Type 'Right.Outer' is not assignable to type 'Left.Outer'. + Types of property 'box' are incompatible. + Type 'RecursiveBox' is not assignable to type 'RecursiveBox'. + Type 'Right.Inner' is not assignable to type 'Left.Inner'. + Types of property 'box' are incompatible. + Type 'RecursiveBox' is not assignable to type 'RecursiveBox'. + Type 'Right.Mid' is not assignable to type 'Left.Mid'. + Types of property 'box' are incompatible. + Type 'RecursiveBox' is not assignable to type 'RecursiveBox'. + Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. + Types of property 'id' are incompatible. + Type 'number' is not assignable to type 'string'. + + +==== selfReferencePreservesFiniteNesting.ts (1 errors) ==== + interface RecursiveBox { + aNext?: RecursiveBox; + zValue: T; + } + + namespace Left { + export interface Outer { + box: RecursiveBox; + } + export interface Inner { + box: RecursiveBox; + } + export interface Mid { + box: RecursiveBox; + } + export interface Leaf { + id: string; + } + } + + namespace Right { + export interface Outer { + box: RecursiveBox; + } + export interface Inner { + box: RecursiveBox; + } + export interface Mid { + box: RecursiveBox; + } + export interface Leaf { + id: number; + } + } + + function test(left: Left.Outer, right: Right.Outer) { + left = right; + ~~~~ +!!! error TS2322: Type 'Right.Outer' is not assignable to type 'Left.Outer'. +!!! error TS2322: Types of property 'box' are incompatible. +!!! error TS2322: Type 'RecursiveBox' is not assignable to type 'RecursiveBox'. +!!! error TS2322: Type 'Right.Inner' is not assignable to type 'Left.Inner'. +!!! error TS2322: Types of property 'box' are incompatible. +!!! error TS2322: Type 'RecursiveBox' is not assignable to type 'RecursiveBox'. +!!! error TS2322: Type 'Right.Mid' is not assignable to type 'Left.Mid'. +!!! error TS2322: Types of property 'box' are incompatible. +!!! error TS2322: Type 'RecursiveBox' is not assignable to type 'RecursiveBox'. +!!! error TS2322: Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. +!!! error TS2322: Types of property 'id' are incompatible. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/typePredicateRecursionPreservesFiniteNesting.errors.txt b/testdata/baselines/reference/compiler/typePredicateRecursionPreservesFiniteNesting.errors.txt new file mode 100644 index 0000000000..b825cf0ad8 --- /dev/null +++ b/testdata/baselines/reference/compiler/typePredicateRecursionPreservesFiniteNesting.errors.txt @@ -0,0 +1,95 @@ +typePredicateRecursionPreservesFiniteNesting.ts(22,7): error TS2322: Type '() => this is Guard & Right.L2' is not assignable to type '() => this is Guard & Left.L2'. + Type predicate 'this is Guard & L2' is not assignable to 'this is Guard & L2'. + Type 'Guard & Right.L2' is not assignable to type 'Guard & Left.L2'. + Type 'Guard & L2' is not assignable to type 'Guard'. + Types of property 'guard' are incompatible. + Type '() => this is Guard & Right.L2' is not assignable to type '() => this is Guard & Left.L2'. + Type predicate 'this is Guard & L2' is not assignable to 'this is Guard & L2'. + Type 'Guard & Right.L2' is not assignable to type 'Guard & Left.L2'. + Type 'Guard & L2' is not assignable to type 'L2'. + Types of property 'next' are incompatible. + Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.L3' is not assignable to type 'Left.L3'. + Types of property 'next' are incompatible. + Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.L4' is not assignable to type 'Left.L4'. + Types of property 'next' are incompatible. + Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. + Types of property 'id' are incompatible. + Type 'number' is not assignable to type 'string'. +typePredicateRecursionPreservesFiniteNesting.ts(23,7): error TS2322: Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.L2' is not assignable to type 'Left.L2'. + Types of property 'next' are incompatible. + Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.L3' is not assignable to type 'Left.L3'. + Types of property 'next' are incompatible. + Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.L4' is not assignable to type 'Left.L4'. + Types of property 'next' are incompatible. + Type 'Guard' is not assignable to type 'Guard'. + Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. + Types of property 'id' are incompatible. + Type 'number' is not assignable to type 'string'. + + +==== typePredicateRecursionPreservesFiniteNesting.ts (2 errors) ==== + interface Guard { + guard(): this is this & T; + } + + namespace Left { + export interface L2 { next: Guard; } + export interface L3 { next: Guard; } + export interface L4 { next: Guard; } + export interface Leaf { id: string; } + } + + namespace Right { + export interface L2 { next: Guard; } + export interface L3 { next: Guard; } + export interface L4 { next: Guard; } + export interface Leaf { id: number; } + } + + declare const left: Guard; + declare const right: Guard; + + const predicateComparison: typeof left.guard = right.guard; + ~~~~~~~~~~~~~~~~~~~ +!!! error TS2322: Type '() => this is Guard & Right.L2' is not assignable to type '() => this is Guard & Left.L2'. +!!! error TS2322: Type predicate 'this is Guard & L2' is not assignable to 'this is Guard & L2'. +!!! error TS2322: Type 'Guard & Right.L2' is not assignable to type 'Guard & Left.L2'. +!!! error TS2322: Type 'Guard & L2' is not assignable to type 'Guard'. +!!! error TS2322: Types of property 'guard' are incompatible. +!!! error TS2322: Type '() => this is Guard & Right.L2' is not assignable to type '() => this is Guard & Left.L2'. +!!! error TS2322: Type predicate 'this is Guard & L2' is not assignable to 'this is Guard & L2'. +!!! error TS2322: Type 'Guard & Right.L2' is not assignable to type 'Guard & Left.L2'. +!!! error TS2322: Type 'Guard & L2' is not assignable to type 'L2'. +!!! error TS2322: Types of property 'next' are incompatible. +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.L3' is not assignable to type 'Left.L3'. +!!! error TS2322: Types of property 'next' are incompatible. +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.L4' is not assignable to type 'Left.L4'. +!!! error TS2322: Types of property 'next' are incompatible. +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. +!!! error TS2322: Types of property 'id' are incompatible. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + const ordinaryComparison: Guard = right; + ~~~~~~~~~~~~~~~~~~ +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.L2' is not assignable to type 'Left.L2'. +!!! error TS2322: Types of property 'next' are incompatible. +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.L3' is not assignable to type 'Left.L3'. +!!! error TS2322: Types of property 'next' are incompatible. +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.L4' is not assignable to type 'Left.L4'. +!!! error TS2322: Types of property 'next' are incompatible. +!!! error TS2322: Type 'Guard' is not assignable to type 'Guard'. +!!! error TS2322: Type 'Right.Leaf' is not assignable to type 'Left.Leaf'. +!!! error TS2322: Types of property 'id' are incompatible. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + \ No newline at end of file diff --git a/testdata/tests/cases/compiler/mergedPolymorphicThisReferences.ts b/testdata/tests/cases/compiler/mergedPolymorphicThisReferences.ts new file mode 100644 index 0000000000..52c51e302a --- /dev/null +++ b/testdata/tests/cases/compiler/mergedPolymorphicThisReferences.ts @@ -0,0 +1,33 @@ +// @noEmit: true +// @noTypesAndSymbols: true +// @strict: true + +// @filename: class.ts +declare class MergedBase {} + +// @filename: interface.ts +type Owned = { owner: T }; + +interface MergedBase { + clone(): this; + isOwned(): this is this & Owned; + queried(value: this, box: { value: typeof value }): void; + conditional(): this extends MergedDerived ? "derived" : "base"; +} + +declare class MergedDerived extends MergedBase { + derived: true; +} + +declare const base: MergedBase; +declare const derived: MergedDerived; + +const cloned: MergedDerived = derived.clone(); +const conditionalValue: "derived" = derived.conditional(); + +// @ts-expect-error `typeof value` must be instantiated from MergedBase to MergedDerived. +derived.queried(derived, { value: base }); + +if (derived.isOwned()) { + const owner: MergedDerived = derived.owner; +} diff --git a/testdata/tests/cases/compiler/selfReferenceExactInstancesPreserveFiniteNesting.ts b/testdata/tests/cases/compiler/selfReferenceExactInstancesPreserveFiniteNesting.ts new file mode 100644 index 0000000000..90be93dccd --- /dev/null +++ b/testdata/tests/cases/compiler/selfReferenceExactInstancesPreserveFiniteNesting.ts @@ -0,0 +1,55 @@ +// @noEmit: true +// @noTypesAndSymbols: true +// @strict: true + +interface Box { + left2(): Box; + left3(): Box; + leftLeaf(): Box; + right2(): Box; + right3(): Box; + rightLeaf(): Box; + value: T; +} + +namespace Left { + export interface Outer { + box: Box; + } + export interface L2 { + box: Box; + } + export interface L3 { + box: Box; + } + export interface Leaf { + id: string; + } +} + +namespace Right { + export interface Outer { + box: Box; + } + export interface L2 { + box: Box; + } + export interface L3 { + box: Box; + } + export interface Leaf { + id: number; + } +} + +declare const box: Box; +box.left2(); +box.left3(); +box.leftLeaf(); +box.right2(); +box.right3(); +box.rightLeaf(); + +function test(left: Left.Outer, right: Right.Outer) { + left = right; +} diff --git a/testdata/tests/cases/compiler/selfReferenceInMethodBody.ts b/testdata/tests/cases/compiler/selfReferenceInMethodBody.ts new file mode 100644 index 0000000000..90f74cdf98 --- /dev/null +++ b/testdata/tests/cases/compiler/selfReferenceInMethodBody.ts @@ -0,0 +1,11 @@ +// @noEmit: true +// @noTypesAndSymbols: true +// @strict: true + +class Box { + value!: T; + + method(source: Box>>) { + const target: Box>> = source; + } +} diff --git a/testdata/tests/cases/compiler/selfReferencePreservesFiniteNesting.ts b/testdata/tests/cases/compiler/selfReferencePreservesFiniteNesting.ts new file mode 100644 index 0000000000..ceed16f779 --- /dev/null +++ b/testdata/tests/cases/compiler/selfReferencePreservesFiniteNesting.ts @@ -0,0 +1,42 @@ +// @noEmit: true +// @noTypesAndSymbols: true +// @strict: true + +interface RecursiveBox { + aNext?: RecursiveBox; + zValue: T; +} + +namespace Left { + export interface Outer { + box: RecursiveBox; + } + export interface Inner { + box: RecursiveBox; + } + export interface Mid { + box: RecursiveBox; + } + export interface Leaf { + id: string; + } +} + +namespace Right { + export interface Outer { + box: RecursiveBox; + } + export interface Inner { + box: RecursiveBox; + } + export interface Mid { + box: RecursiveBox; + } + export interface Leaf { + id: number; + } +} + +function test(left: Left.Outer, right: Right.Outer) { + left = right; +} diff --git a/testdata/tests/cases/compiler/selfReferentialInstanceMemberRecursion.ts b/testdata/tests/cases/compiler/selfReferentialInstanceMemberRecursion.ts new file mode 100644 index 0000000000..9ba516d24a --- /dev/null +++ b/testdata/tests/cases/compiler/selfReferentialInstanceMemberRecursion.ts @@ -0,0 +1,82 @@ +// @noEmit: true +// @noTypesAndSymbols: true +// @strict: true +// @target: esnext + +type BodyStyle = + | "sedan" | "coupe" | "hatchback" | "suv" | "wagon" | "pickup" + | "minivan" | "crossover" | "limousine" | "roadster" | "convertible"; +type SportBodyStyle = "hatchback" | "roadster" | "minivan" | "crossover" | "wagon" | "pickup" | "convertible"; + +type ModelId = string; +type Injector = "a" | "b"; +type Intercooler = { type: string; attribute: string }; +type Transmission = { savesOn: boolean; filter: unknown }; +type Grille = { range?: unknown } | { empty: true } | { unfiltered: true }; +type Gasket = Record; +type PaintScheme = [T] extends ["pickup"] + ? { attributes: Intercooler[] } + : Intercooler; +type Hubcap = [T] extends [SportBodyStyle] ? Gasket : { type: ModelId }; +type Trim = T extends unknown ? `${T}-trim` : never; +type TrimSet = [T] extends [SportBodyStyle] + ? { [K in Trim]?: string } + : { [K in Trim]?: string }; +type TrimEnabled = [T] extends [SportBodyStyle] + ? { [K in Trim]?: { enabled: boolean } } + : { [K in Trim]?: { enabled: boolean } }; +type Hood = T extends "sedan" | "suv" | "wagon" | "pickup" | "hatchback" | "minivan" | "crossover" + ? { style: T } + : never; + +interface CarSpec { + paintScheme?: PaintScheme; + sortOrder?: Hubcap; + modelYear?: { groupingOn?: boolean; filter?: Grille }; + saves?: { savesOn: boolean } | Transmission; + types?: T extends SportBodyStyle ? ModelId[] : never; + bodyStyle?: T; + sharedConfig?: { modelYear?: unknown }; + trim?: TrimSet; + trimEnabled?: TrimEnabled; + modelOptions?: Hood; +} + +type DealerTrim = any; +type DealerTrims = Record; +type WithRequired = Required> & O; + +interface Car extends CarSpec {} +declare class Car { + isRoadster(): this is Car<"roadster">; + isConvertible(): this is Car<"convertible">; + hasModelYear(): this is WithRequired & { + modelYear: { groupingOn: true }; + }; + hasPickupPaint(): this is WithRequired, "paintScheme"> & { + paintScheme: { attributes: Intercooler[] }; + }; + hasPickupSetting(): this is (WithRequired, "modelYear"> & { + modelYear: { groupingOn: true }; + }) | (WithRequired, "saves"> & { + saves: { savesOn: true }; + }); + hasCustomPaint(): this is WithRequired>, "paintScheme">; + getTrims( + trims: DealerTrims, + trim?: TrimSet, + trimEnabled?: TrimEnabled, + filter?: (key: Trim, definition: DealerTrim) => boolean, + ): Record; + hasTransmission(): this is Car & { saves: Transmission }; + hasFilter(): this is Car & { modelYear: { filter: Grille } }; + hasSort(type: Injector): this is Car & { sortOrder: Gasket }; + hasSportSort(): this is Car & { sortOrder: Gasket }; +} + +declare class CarInspector extends Car {} + +export function repro(car: CarInspector): boolean { + if (car.isRoadster() || car.isConvertible()) {} + return car.hasCustomPaint(); +} diff --git a/testdata/tests/cases/compiler/typePredicateRecursionPreservesFiniteNesting.ts b/testdata/tests/cases/compiler/typePredicateRecursionPreservesFiniteNesting.ts new file mode 100644 index 0000000000..8e6a5bd57a --- /dev/null +++ b/testdata/tests/cases/compiler/typePredicateRecursionPreservesFiniteNesting.ts @@ -0,0 +1,27 @@ +// @noEmit: true +// @noTypesAndSymbols: true +// @strict: true + +interface Guard { + guard(): this is this & T; +} + +namespace Left { + export interface L2 { next: Guard; } + export interface L3 { next: Guard; } + export interface L4 { next: Guard; } + export interface Leaf { id: string; } +} + +namespace Right { + export interface L2 { next: Guard; } + export interface L3 { next: Guard; } + export interface L4 { next: Guard; } + export interface Leaf { id: number; } +} + +declare const left: Guard; +declare const right: Guard; + +const predicateComparison: typeof left.guard = right.guard; +const ordinaryComparison: Guard = right;