Repository navigation
Closed as not planned
Labels
Not a DefectThis behavior is one of several equally-correct optionsThis behavior is one of several equally-correct options
Description
π Search Terms
infer, extend, class, interface, methods, type parameters, generics
π Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about inference
β― Playground Link
π» Code
// I have a class using builder pattern that I'm trying to extend
declare class Def<T> {
a(): Def<T>;
b<SearchField extends T>(param: SearchField): Def<T>;
}
// The extended class has to redeclare all methods, because it has a more
// concrete subtype (it has additional methods, not shown here)
declare class ExtendDef<T> extends Def<T> {
a(): ExtendDef<T>;
b<SubT extends T>(param: SubT): ExtendDef<T>;
}
declare const x: ExtendDef<"hello">;
// Both of these should be true, but they are not
type DoesItExtend = typeof x extends Def<any> ? true : false;
type CanItBeInferred = typeof x extends Def<infer F> ? true : false;
assert<Equals<DoesItExtend, true>>;
assert<Equals<CanItBeInferred, true>>;
// Ok, that's surprising, but this is even more surprising:
declare class ExtendDef2<T> extends Def<T> {
a(): ExtendDef<T>;
// no b()
}
declare const x2: ExtendDef2<"hello">;
// This works!
assert<Equals<typeof x2 extends Def<infer F> ? true : false, true>>;
// Ok, ok, but this is even even more surprising:
declare class ExtendDef3<T> extends Def<T> {
// a(): ExtendDef<T>;
b<SubT extends T>(param: SubT): ExtendDef<T>;
}
declare const x3: ExtendDef2<"hello">;
// This works too!
assert<Equals<typeof x3 extends Def<infer F> ? true : false, true>>;
// As long as I don't override at least one parent method,
// the inference continues working. Unfortunately I cannot do this
// in my code, I !have! to override all the parent methods.
//// START: Type-checking utils ////
type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function assert<T extends true>() {
// no need to do anything! we're just asserting at compile time that the type
// parameter is true.
}
//// END: Type-checking utils ////π Actual behavior
Inference is broken when I override all methods.
π Expected behavior
Inference should continue to work.
Additional information about the issue
This is blocking a library release I'm trying to make.
Also I cannot change the parent to have this in method return types, because the return types actually change (the code has been simplified from hundreds of LOCs in our project).
Metadata
Metadata
Assignees
Labels
Not a DefectThis behavior is one of several equally-correct optionsThis behavior is one of several equally-correct options
Activity
DanielRosenwasser commentedon Jan 12, 2024
My guess is that during inference, there are no places to actually infer types from directly.
With identical type references like
Def<Blah> => Def<T>, the type system can bail out early because it knowsBlahcan be inferred toT.As the type system tries to relate an
ExtendDefto aDef, it does so structurally. As it descends into the type, it finds no useful information in eitheraorb. It can't infer from the bounds of your generics, and where it matches up anExtendDefto aDefthe type system finds itself doing work it was already in the middle of.One easyish fix is to just have something like
declare class Def<T> { a(): Def<T>; b<SearchField extends T>(param: SearchField): Def<T>; + protected " _unused"!: T }You unfortunately cannot declare this as a
privatefield because TypeScript makes the assumption that the types of private fields can be erased away in declaration emit, which would make inference fail again.Anyway, this is more of a guess. Not sure if Ryan Cavanaugh (@RyanCavanaugh) wants to take a look.
Side note, in the code you've given, you're using a lot of type parameters that are used as inputs, but not in the return types. I would get rid of those if possible.
xixixao commentedon Jan 12, 2024
Thanks Daniel Rosenwasser (@DanielRosenwasser), I boiled this down from a much more complicated code. The type parameters are constrained in reality, and they are used in the type of the class, which is then used (not refuting your analysis though, I don't understand the typechecking internals well enough).
You can find real
Defhere: https://github.com/get-convex/convex-js/blob/main/src/server/schema.ts#L151-L311And
ExtendDeflooks like this (with additional methods):Nice! It's good to know I don't even need to declare a method to get around this on the parent.
xixixao commentedon Jan 12, 2024
Hmm, so I also have to use
return this: anyin the parent for this to work:playground link
Code:
Edit: Adding a method without adding fields seems to be a better workaround for now:
RyanCavanaugh commentedon Jan 12, 2024
I'd need a concrete example where inference was failing the presence of what could have been a deducible structural inference to classify this as a bug
xixixao commentedon Jan 12, 2024
Ryan Cavanaugh (@RyanCavanaugh) Does this example help? playground link
RyanCavanaugh commentedon Jan 12, 2024
All uses of
TinDefare self-referential or in constraint positions, so noxixixao commentedon Jan 12, 2024
What would make
Tnot be those? How come uncommentingm()changes things? (sorry I'm not that familiar with the type-checking details)typescript-bot commentedon Jan 15, 2024
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.