You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Higher Order Instantiation Expressions don't resolve types correctly #52035
declarefunctiontransform<constAextendsreadonlyany[],Fextends<VextendsA[number],KextendskeyofA,>(value: V,index: K,array: A)=>any>(array: [...A],func: F): {[KinkeyofA]: ReturnType<typeoffunc<A[K],K>>};const$2=transform(["object","string","number"],k=>({type: k}));// ^?// Should be [{ type: "object" }, { type: "string" }, { type: "number" }], but is [any, any, any]
🙁 Actual behavior
In the return type annotation of transform(), the instantiation expression (added by Instantiation expressions #47607) typeof func<A[K], K> reverts to it's generic constraint type of F (<V extends A[number], K extends keyof A>(value: V, index: K, array: A) => any) when it is invoked. This causes the return type of transform() to be a tuple of any instead of { type: A[K] }.
It seems TypeScript currently has no support for higher order types, and it's a design limitation. That means, the following code, or other code with the same functionality (like the example in this issue), will not work as we expected.
typeMapped<Mapper,T>=Mapper<T>;// Type 'Mapper' is not generic. (2315)
It's common to make abstraction like this in JavaScript:
functionmapped(mapper,value){returnmapper(value);}functionbox(value){return{ value };}console.log(mapped(box,'foo'));
That is, we might want to create higher order function called mapped that applies the mapper to the given value, and both mapper and value are not statically typed, and the type Mapped above is what we might want to.
Another situation is class instantiation with constructors and constructor parameters. Generic function works well with normal constructors, but it cannot infer generic instance type with generic constructors:
functioncreate<Textendsnew(...args: any[])=>any>(ctor: T, ...params: ConstructorParameters<T>): InstanceType<T>{returnnewctor(...params);}classFoo{constructor(publicmessage: string){}sayHello(){console.log(`hello from ${this.message}`);}}// Well typed, because `Foo` is not a generic type.constfoo=create(Foo,"bar");foo.sayHello();// Not well typed, because `Set` is a generic type, and its constructor parameter uses its generic parameter `T`.constnumberSet=create(Set,[1,2,3]);// ^^^^^^^^^ Inferred as `Set<unknown>` -- the generic parameter is instantiated with its constraint.
TypeScript 4.7 introduced instantiation expressions, which seems to make that possible: instantiate generic parameters of a generic function by instantiation expressions, and then get the return type of the generic function. But unfortunately, it didn't make it.
I know it's not the first time to introduce higher order types (or higher order generics) in the TypeScript community and it takes a lot of effort to implement this feature, but I still wish that TypeScript would support it, which would absolutely open a new world for the type system like the template literal types feature in TypeScript 4.1✨.
The thing you mostly need here is call types, not higher-kinded types per se. The very smallest problem is that k => { type: k } is not a generic function, and what needs to happen in this call to resolve as desired is to repeatedly perform separate generic inference rounds on calls to that function expression (once you make it generic, of course).
Tracking somewhat at #52295 (this would be a mapped type rather than a union type, but the logic is pretty similar) and to a greater extent at #6606, which I think we will revisit sometime to specifically think about (generic) functions' call types.
I don't think you understood my example; the issue is not strictly mapped or union types. Even though k => { type: K } is not generic, it should be "inhabited" by the generic constraint type (hovering over shows it already does sort of). This inhabiting behavior already happens with satisifies, as in this example:
typeFunc=<T>(value: T)=>any;constsatisfiedFunc=(param=>{return[param]asconst;})satisfiesFunc;type$0=typeofsatisfiedFunc<0>;// outputs correctly as `readonly [0]`
To further illustrate the issue, the expected behavior actually happens when you explicitly type the return type of F (in this case { type: V }:
// ...Fextends<VextendsA[number],KextendskeyofA,>(value: V,index: K,array: A)=>{type: V}|{}// ...constitems=["object","string","number"]asconst;const$0=transform(items,k=>({type: k}));// ^?// Correctly becomes [{ type: "object" }, { type: "string" }, { type: "number" }], because the type is explicitly typed in the generic constraint// It should go to resolve to the type of `F` though
The correct type of $0 would be { type: "obj" } | { type: "num" } | { type: "str" }
Since Lookup could be arbitrarily complex (e.g. a conditional type instead of a lookup type), the only correct way to resolve this would be to iteratively pretend to call the function expression with each type as it goes through [K in keyof A]: ReturnType<typeof func<A[K], K>>
Activity
Thundercraft5 commentedon Dec 27, 2022
Additional repro demonstrating this bug.
P.S. It seems that cases for higher-order expressions weren't included in baselines for instantiation expressions.
DarrenDanielDay commentedon Dec 29, 2022
It seems
TypeScriptcurrently has no support forhigher order types, and it's a design limitation. That means, the following code, or other code with the same functionality (like the example in this issue), will not work as we expected.It's common to make abstraction like this in
JavaScript:That is, we might want to create higher order function called
mappedthat applies themapperto the givenvalue, and bothmapperandvalueare not statically typed, and the typeMappedabove is what we might want to.Another situation is class instantiation with constructors and constructor parameters. Generic function works well with normal constructors, but it cannot infer generic instance type with generic constructors:
TypeScript 4.7introducedinstantiation expressions, which seems to make that possible: instantiate generic parameters of a generic function by instantiation expressions, and then get the return type of the generic function. But unfortunately, it didn't make it.I know it's not the first time to introduce
higher order types(orhigher order generics) in theTypeScriptcommunity and it takes a lot of effort to implement this feature, but I still wish thatTypeScriptwould support it, which would absolutely open a new world for the type system like thetemplate literal typesfeature inTypeScript 4.1✨.RyanCavanaugh commentedon Jan 24, 2023
The thing you mostly need here is call types, not higher-kinded types per se. The very smallest problem is that
k => { type: k }is not a generic function, and what needs to happen in this call to resolve as desired is to repeatedly perform separate generic inference rounds on calls to that function expression (once you make it generic, of course).Tracking somewhat at #52295 (this would be a mapped type rather than a union type, but the logic is pretty similar) and to a greater extent at #6606, which I think we will revisit sometime to specifically think about (generic) functions' call types.
Thundercraft5 commentedon Jan 25, 2023
I don't think you understood my example; the issue is not strictly mapped or union types. Even though
k => { type: K }is not generic, it should be "inhabited" by the generic constraint type (hovering over shows it already does sort of). This inhabiting behavior already happens withsatisifies, as in this example:Playground Link
To further illustrate the issue, the expected behavior actually happens when you explicitly type the return type of
F(in this case{ type: V }:Playground Link
RyanCavanaugh commentedon Jan 26, 2023
What you're seeing there is an illusion. Consider tweaking it to this variant:
The correct type of
$0would be{ type: "obj" } | { type: "num" } | { type: "str" }Since
Lookupcould be arbitrarily complex (e.g. a conditional type instead of a lookup type), the only correct way to resolve this would be to iteratively pretend to call the function expression with each type as it goes through[K in keyof A]: ReturnType<typeof func<A[K], K>>typescript-bot commentedon Jan 28, 2023
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
ntucker commentedon Apr 19, 2023
This is a problem:
https://www.typescriptlang.org/play?target=99&jsx=0&ts=5.0.4#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwxilQGdEcYBbAHgCh4H4w8SN4BBeEADwxFWAl4cKMDwQAnvGISA2gF0ANPHqMAYl179B8agDUAfAAoAblAjIQALnh6AlPAC8B6agm1jsIhJuyAdAHsSkhoYDZqdjYA3vCyANLwWPgA1iASOIgc8jYASiAYyDCoACoSAA4g1BjlIBkh6NTs8fIGLgC+ANy0tMykbJmOusXGyVbFDs7wRjHVFTbJ8G12PSxsACQATE4ERKTkVEayAEQ4AEYAVuAYR8pHrDBJAOY38EeoyJSnIDBHwYh2XUY8AA9MD4AA9AD8qgYoPgAGUABY4ZAQYDwL6xGY1GwnC5XI6LZTYuave5PQltYkEHGvd6fb6U+RAA
When I change to:
it works - but that assumes we actually know the return value - if we're having the entire F be generic - maybe it'll have a different return type!
This simply makes the second call fail...but if we make the generic accept that, then it won't give us a return type
Final example with both: https://www.typescriptlang.org/play?target=99&jsx=0&ts=5.0.4#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwxilQGdEcYBbAHgCh4H4w8SN4BBeEADwxFWAl4cKMDwQAnvGISA2gF0ANPHqMAYl179B8agDUAfAAoAblAjIQALnh6AlPAC8B6agm1jsIhJuyAdAHsSkhoYDZqdjYA3vCyANLwWPgA1iASOIgc8jYASiAYyDCoACoSAA4g1BjlIBkh6NTs8fIGLgC+ANy0tMykbJmOusXGyVbFDs7wRjHVFTbJyjyEUDYArPBtdj0s-QBMTkMjYxMu0-AARtAwAObzG1u9rPAAJPuDy6TkVEayAEQ45wAVuAML9lL9WDAktcwfBfqhkJRziAYL9gog7F1GPAAPQ4+AAPQA-KoGHj4ABlAAWOGQEGAFwQshmNRs-yBINhSyINgRSJRG2ULLmcMh0K5vB58D5yJggvgwuscJlKIly15iNlG3k2z6LwAzAcPmQKJQfuzgZhYRDCOLwSrUejdpjSbj8cTXeTqbT6YzYjFLrBbnCAZbQfKA1dgzaoagYRGLlG2Q7ftrurRQJBYAgUOhsHgCERPqbShVgHRsY82JwltohCIxKhJK45MFXRpawIhPpjGYLEr7E4XFFZgO2h4jF4oD5YgE-EFlLmwvAItFYgkkvBUulMkFcvlCiUalUanUl41mq0Nl1dU9ngAWI1Fk1UUsgYDm0Oc8FiuPWlPoi62Lkh6wH4t6dIMsi-oEKyIYclaiySis0qagKbRCrBIoxnamjqqh-JyhhCpYUq8JoaiSH4Q6aZVi86zvM+XyUG+H5-F+iGiraf72hRaKLs6WKMCBJJgZSNKQX6zKJkGbIceGxGRrJXGxvGikyTcyZ8dqQA