Case for inference failure in `T extends F<T>`
Bug Report
🔎 Search Terms
circular type parameter constraint
Related issues: #40439 #30134
🕗 Version & Regression Information
Tested with 4.8.4
⏯ Playground Link
💻 Code
declare const f:
<T extends F<T>>(t: T) => T
type F<T> =
{ a: unknown
, b: (a: T extends { a: infer X } ? X : never) => unknown
}
f({
a: "hello",
b: x => x.toUpperCase()
})
// doesn't compile because infers `x` as `unknown` instead of `string`
f({
a: "hello",
b: (x: string) => x.toUpperCase()
})
// compiles
Note that there is a workaround but it's only a workaround for this minimal case and not for the real world case from which this minimal case was derived.
🙁 Actual behavior
x in the first f call gets inferred as unknown
🙂 Expected behavior
x in the first f call should get inferred as string
In TypeScript, generics usually work just fine, however when you start using already extended generic types to define new generic types that will also be extended - this is where the trouble occurs. In general, Generic Constraints (e.g. <T>) in TypeScript are not considered as types that can be inferred further, meaning that whilst extending an extended type like you have in f4 is harmless, extending a newly defined type that also uses generics is problematic:
f4 is perfectly fine:
<Type1 extends Type2, Type2 extends OrderType>
f3 is also fine as it is not extending the type to define the callback but is instead creating directly:
f: (t: Type) => Promise<Type>
f2 is where the problem begins to occur as the parameter type is using an extended generic type that also uses extended values. This causes the type to default itself back to OrderType and because FullType does not directly equal OrderType you are observing that error:
Fn extends (t: Type) => Promise<Type>
Btw one can write a class equivalent of this too...
interface F
{ a: unknown
, b: (a: this["a"]) => unknown
}
new class implements F {
a = "hello"
b = x => x.toUpperCase()
}
// doesn't compile because `x` is inferred as `any` instead of `string`.
new class implements F {
a = "hello"
b = (x: string) => x.toUpperCase()
}
// compiles
@RyanCavanaugh Could I use this thread to ask some quick trivial questions about the codebase? I'm giving a silly shot at writing a PR for this. The question I have right now is how do you create a type from a type alias symbol and a type passed in as an argument? I imagine something like this...?
let yType = createType(0) // What flags to pass in?
yType.aliasSymbol = fSymbol
yType.aliasArguments = [xType]
yType.__debugTypeToString() does give me "F<X>" but I'm not sure if this is the right way or if it works.
Also let me know if there's a better place/way to ask questions like this.
Edit: I don't need the answer anymore turns out I didn't exactly needed it (see #51511), but still I'd be bonus if I get to know the answer anyway :P
Edit 2: Ah I think getTypeAliasInstantiation(fSymbol, [xType]) would do the trick.
This would be helpful for excluding certain types in a generic type T. For example Optional<null> doesn't really make sense so it would be nice if we could tell TypeScript that some types should not be allowed for generic type T
Example:
type Optional<T extends NonNullable<T>> =
| { kind: 'Some'; value: T; }
| { kind: 'None'; };
type OptionalNull = Optional<null>;
// ^^^^
// Error: Type 'null' does not satisfy the constraint 'NonNullable<null>'.
// Because type 'null' does not satisfy the constraint 'never'.
type OptionalNull = Optional<null | string>;
// ^^^^^^^^^^^^^
// Error: Type 'null | string' does not satisfy the constraint 'NonNullable<null | string>'.
// Because type 'null' does not satisfy the constraint 'string'.
Your comment/usecase belongs to #51011.
This issue is for this specific inference failure in the OP, and here T extends F<T> is already allowed because it's not immediately circular.