Repository navigation
Defining a type predicate breaks inheritance for methods with mapped typesΒ #56133
Description
Activity
Andarist commented
on Oct 17, 2023 ContributorMore actionsIt's not the type predicate but rather the reference to
T. The same happens withabstract test(): Tand similar.Note that your
xyzzymethod returnsArbitrary<Us>and that becomesTwithinArbitrary. I think that, in a way, you are asking for this to be allowed:const item: Arbitrary<string> = {} as MyArbitrary<number>
We can see a somewhat better error here as it at least refers to the method that references
Tbut the origin of the problem is truly in the return type ofxyzzyRyanCavanaugh commented
on Oct 17, 2023 MemberMore actionsThis seems like a bug:
// No constraint: OK { interface Arbitrary<T> { cov: T; xyzzy<U>(x: { [K in keyof U]: U[K] }): Arbitrary<U>; } class MyArbitrary<T> implements Arbitrary<T> { cov!: T; xyzzy<U>(x: { [K in keyof U]: U[K] }): Arbitrary<U> { return null as any; } } } // Use a type alias: OK { type SomeMap<U> = { [K in keyof U]: U[K] }; interface Arbitrary<T> { cov: T; xyzzy<U extends unknown[]>(x: SomeMap<U>): Arbitrary<U>; } class MyArbitrary<T> implements Arbitrary<T> { cov!: T; xyzzy<U extends unknown[]>(x: SomeMap<U>): Arbitrary<U> { return null as any; } } } // Neither: Error { interface Arbitrary<T> { cov: T; xyzzy<U extends unknown[]>(x: { [K in keyof U]: U[K] }): Arbitrary<U>; } class MyArbitrary<T> implements Arbitrary<T> { cov!: T; xyzzy<U extends unknown[]>(x: { [K in keyof U]: U[K] }): Arbitrary<U> { return null as any; } } }
Reacted by ExE Boss- addedBugA bug in TypeScriptA bug in TypeScriptHelp WantedYou can do thisYou can do this
on Oct 17, 2023 BTW, I noticed last night the weirdness that is
{ [K in keyof U]: U[K] }being a different type from justU, but it definitely is because it extendsobjecteven whenUitself doesn't. Not sure if it's related but it sure seems fishy. I found that pattern in fast-check, which seems like a fairly high-profile library. It's in the defintion of a user-facing function inrecord.ts:function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<RecordValue<{ [K in keyof T]: T[K] }>>;
I think the variable
Treally should require anextends objectconstraint, since I don't see how a call to that function could possibly type check otherwise. My guess is that using a trivial mapped type is a workaround someone discovered to avoid introducing that constraint for whatever reason.My guess is that using a trivial mapped type is a workaround someone discovered to avoid introducing that constraint for whatever reason.
I don't know if this is the case here but
type ID<T> = { [K in keyof T]: T[K] };
is a common pattern people sometimes write to get TS to fully expand object types in hover tips. AFAIR it's just a no-op on primitive types so it's nice to be able to use it in situations where an
objectconstraint would be inconvenient.gabritto commented
on Oct 25, 2023 MemberMore actionsI'm looking into this, so far I think the discrepancy in #56133 (comment) is due to inference, which we perform when we compare two signatures that have type parameters (in this case the signatures for
xyzzy).gabritto commented
on Nov 11, 2023 MemberMore actionsOk, as I mentioned above, the reason why the code errors is, I think, because of inference.
What happens when type checking the code is roughly the following:
-
We check that
MyArbitrary<T>is assignable toArbitrary<T>, becauseMyArbitrary<T>extendsArbitrary<T>.-
When doing that, we check if the type of property
xyzzyofMyArbitrary<T>is assignable to the type of the same property inArbitrary<T>.-
To check if the types of property
xyzzyare assignable, we check if source signature<Us extends unknown[]>(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>(fromMyArbitrary<T>) is assignable to target signature<Us extends unknown[]>(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>(fromArbitrary<T>).-
Because the source signature has type parameters that are different from the target signature's type parameters, when checking that the source is assignable to the target, we instantiate the source signature in the context of the canonical form of the target signature (i.e. we call
instantiateSignatureInContextOf), where the canonical form of the target signature is(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>.- During
instantiateSignatureInContextOf, we have source signature<Us extends unknown[]>(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>, target signature(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>. This is where type inference kicks in: whatinstantiateSignatureInContextOfdoes is essentially to infer type arguments for the source signature's type parameters, and then instantiate the source signature with those inferred type arguments. It infers the source's type arguments by inferring (i.e. callinferTypes) between the source's parameter types and the target's parameter types, and inferring types between the source's return type and the target's return type. This is the part that causes the problem: we infer type argumentunknown[]for source's type parameterU, and then we instantiate the source signature with this inference, obtaining a new source signature(x: unknown[]): Arbitrary<unknown[]>.
- During
-
We then check if
(x: unknown[]): Arbitrary<unknown[]>is assignable to(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>, and find that it is not assignable, because the source's return typeArbitrary<unknown[]>is not assignable to the target's return typeArbitrary<Us>, becauseArbitrary<T>is covariant onT. This is why the error goes away if we removeabstract canShrinkWithoutContext(value: unknown): value is TinsideArbitrary:Arbitrarybecomes independent ofT, and then we find thatArbitrary<unknown[]>is assignable toArbitrary<Us>.
-
-
-
All the variants in this comment that make the error go away do so by changing how we infer the type arguments for the source signature during
instantiateSignatureInContextOf, making it so that we infer type argumentUs(from the target signature) for type parameterUsin the source signature. We end up checking if(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>is assignable to(x: { [K in keyof Us]: Us[K] }): Arbitrary<Us>(where bothUsare the same type parameter), which is true, so everything works.-
DanielRosenwasser commented
on Nov 17, 2023 MemberMore actionsLet's try to use a cleaner example for all discussions here:
declare class Base<T> { someProp: T; method<U extends unknown[]>(x: { [K in keyof U]: U[K] }): Base<U>; } declare class Derived<T> extends Base<T> { method<U extends unknown[]>(x: { [K in keyof U]: U[K] }): Base<U>; }
Let's use unique type parameter names when discussing issues related to unification:
declare class Base<T> { someProp: T; method<U extends unknown[]>(x: { [K in keyof U]: U[K] }): Base<U>; } declare class Derived<S> extends Base<S> { method<V extends unknown[]>(x: { [K in keyof V]: V[K] }): Base<V>; }
Makes it clearer that
UandVneed to be unified before we can relate the two signatures.- addedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Dec 14, 2023 - locked as resolved and limited conversation to collaborators
on Oct 22, 2025
π Search Terms
type predicate mapped
π Version & Regression Information
β― Playground Link
https://www.typescriptlang.org/play?ts=5.2.2#code/IYIwzgLgTsDGEAJYBthjAgglEBLawUAngDwAqAfAgN4BQCCokM8SwAdgMoAWUu7AawDq+bgHsArhADCY9hACmADwgAKAG7BkEhQC4EEwezEB3dgEp9m7QoS4MZANy16jcAVZKiAL2+kAqhjKiuwAJhiGAsZmANoAuhSqrgxK+tQIMQDSduwIAgpEYgBmCIFx+oFZcQgAvgA0rpZYOPgwxCSBFM41LkweiChoGACyRNh4BO2UCMEKYRjjrYSk03QMfSyIXr4BQSpz4QZGpuzxickIqTQZ2fx5BcWlYOVPVbUNDE2Lk7tdtD1AA
π» Code
π Actual behavior
The compiler reports an error in
MyArbitraryunlesscanShrinkWithoutContextis removed or its return type is changed.π Expected behavior
The
xyzzymethod should be overridden in the derived class.Additional information about the issue
No response