TypeScript Version: 3.7.2
Search Terms:
Passing this to Generic Interface with extended Type Parameter
Code
namespace ErrorDemo {
interface IRelation<T extends ITest> {
relatedThing: T
}
interface ITest<T extends object = object> {
relation: IRelation<this> // Error: Type 'ITest<T>' is not assignable to type 'ITest<object>'.
validate<U extends T>(thingToValidate: U): object extends T ? object : U
}
function doesNotUseTypeParam(thing: ITest) {
const validated = thing.validate({ randomProp: 100 }) // object - this is what we want
}
function usesTypeParam(thing: ITest<{ example: string }>) {
const validated = thing.validate({ example: 'hello' }) // { example: string }
}
}
namespace NoErrorButNotDesired {
interface IRelation<T extends ITest> {
relatedThing: T
}
interface ITest<T extends object = object> {
relation: IRelation<this> // No error
validate<U extends T>(thingToValidate: U): U // Note that `object extends T` is no longer here
}
function doesNotUseTypeParam(thing: ITest) {
const validated = thing.validate({ randomProp: 100 }) // { randomProp: number }, but wanted object
}
function usesTypeParam(thing: ITest<{ example: string }>) {
const validated = thing.validate({ example: 'hello' }) // { example: string }
}
}
Expected behavior:
In the above example, we want the return type of validate to be different based on whether the user passed a type parameter to the interface. An intuitive way to test this seems to be to check whether the parameter's default type extends the parameter itself (which theoretically should only be true if the parameter IS the default).
Actual behavior:
Mysteriously, as soon as object extends T is used for the type of validate, an error is thrown on the this parameter being passed to the generic IRelation interface. The types in the functions ARE actually shown to be what we want them to be, despite the error.
It's possibly worth pointing out that the above example is clearly a simplified example to demonstrate the error. Our actual use case involves trying to have the return type of an interface's function have more specific control over whether an object's keys are required or not. The above example, while likely not useful in the real world, is a very close representation of what we're doing.
Playground Link:
http://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=38&pc=1#code/HYQwtgpgzgDiDGEAEBRATmg9mgIhMmSA3gLABQSlSAlsAC4RoBmCyAkgEoQA2Id1mYAB4AKkggAPBsAAmUJGxHQ6APmLkqmpGh58IMkQAtaAcwBcSERqoBfctcq0GzVgqVQ6o8VIiz5mACMAKwh4OiQAXiRAkLC1UgotSh1efkELTl004TpjKDVKAHpC1AxsCxEATxhkAHJFZVEVWpp5YExwkCgoahNQAO5kOkI6arqGjyEY0NVagDoHLQA3EG5qGT0hAFVvaTlLFQAKXNMRTAA1VfW9Cy2ASgtpsN3ffbEAfmjgmaRbxbsyIsmABXYBhATAJAyTDQAByHS2UAgVRqAAUQGhwMdjMBzG5lHd1IkkvBBB4kCs1hsGDJIkgTri5pTrgxDkRtCBZJgwKisDALABGAAMQqQNkJSGKX1idH+9mJSBBYOySGBSKgKIg6MxYGxpgy7k87Mk4BggwsHjQpjFKkJCSSlFJwHJzOp+jpDJMTKubrZ3lN5qQtUMPG4mBa4qKJWNEgDEAtdCtuLFcrIAPIoEgsFc8PQWDQACFgXR4XQ8D0dLT7VQnIwWIgFFxUhCvJI9vIJqoiQ7tFl9EZ9ZZU4tay4G53Wz4-NKflEnl3q0kUnwIRkmyvBEITvkqFL4eIymhFppXZsdm3XvIREdPWdLlSbkh7rdNHuOkNDHwkAADecvaciN+rRIO0SBhrijBICGOjDgqSrgoIUIwlApaIsiYzalinoGgS3YOk6Lo+notJRJ63oPqy7KYlyPJ8oKIpihKUpUZy0K0Zg-IgcCYABJBNgADRIAExZIAA7pyNIzmEsGaPBKpqtAmqYbq2H4pMMZxgmSYmDadrHlQBHhKekmkTiXrGRAfommAZrxkGIbcGGEZMdG-o2YGlrWgCmgAgCQA
Related Issues:
I've seen various comments that I thought might be related, but they all seem to deal with the contravariance of keyof - but I'm not aware of how keyof could possibly be related to the above example.
Technically this is a duplicate of #31251.
TLDR: The conditional type in the return of validate marks T as invariant. You can fix this using the following (requires --strictFunctionTypes).
interface ITest<T extends object = object> {
relation: IRelation<this>;
validate<U extends T>(thingToValidate: U): ((x: T) => void) extends ((x: object) => void) ? object : U;
}
Wow @jack-williams that is spectacularly effective (while also spectacularly confusing 馃檪)
That's definitely enough for me to roll with for now - thanks a ton!
Ok - follow up question...
I see that your workaround does indeed help us accomplish what we want, but is that ONLY working because of the bug that you linked? When that issue is fixed, will this workaround still work?
Another way of asking this: in this conditional, is Test meant to be true or false?
type Test = ((param: true) => void) extends ((param: boolean) => void) ? true : false
@jack-williams Do you happen to have any other info in response to my previous follow up question?
Basically I'm trying to figure out what the eventual behavior will theoretically be after https://github.com/microsoft/TypeScript/issues/31251 is fixed
Sorry @rdhelms!
Another way of asking this: in this conditional, is Test meant to be true or false?
Test should always be false under strict function types, and there are no issues that will change that.
The workaround I posted works independently of the bug in #31251, as that only targets conditional types with parameters in extends types.
The workaround is not 100% perfect because the type as written is fundamentally unsound. In IRelation<this> the parameter this is not guaranteed to satisfy the constraint ITest<object>. If you ignore the return type in validate you end up trying to relate:
validate<U extends T>(thingToValidate: U): unknown // ignore return
to
validate<U extends object>(thingToValidate: U): unknown // ignore return
which is not sound because the constraint in the target is less specific than the source. A minimal example is:
const a: <U extends 3>(x: U) => void = (x) => {
// x can only be 3
}
const b: <U extends number>(x: U) => void = a; // error, otherwise x could be any number
The proposed workaround works because check types in conditional types are related bivariantly, so when you measure the variance for the modified version T comes out as bivariant, where previously it was invariant. For example, the following should error, but it does not:
declare const foo: ITest<{ x: 'string' }>;
const bar: ITest<{ x: string }> = foo;
That being said, there are currently no PR's planned to address the variance for conditional check-types, so there is nothing planned that will break the workaround. I think alot of code could face regressions if this ever changed.
Thanks @jack-williams that's a very useful example and discussion. I think I'm only beginning to start to wrap my head around the different contexts and subtleties for how TS checks for invariance, but this helps clarify a few points. I appreciate the notes.
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
Most helpful comment
Technically this is a duplicate of #31251.
TLDR: The conditional type in the return of
validatemarksTas invariant. You can fix this using the following (requires--strictFunctionTypes).