-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
type-level function application #17961
type-level function application #17961
Conversation
74a048a
to
6165eee
Compare
Apologises for the potentially silly question/observation, but getting rather scared by the number of changed files I noticed there's a lot of |
6165eee
to
080b391
Compare
@christyharagan: No, I messed up my rebase, somehow it showed all changes on master since I branched off. Fixed it now. |
@tycho01 I was also hacking around this feature a little bit some time ago, although with slightly different syntax ( |
@tycho01 - Wow! So is this going to be the one that lets me get past atrauzzi/protoculture-react#5? :) |
@Igorbek: I added the current issues to my original post now -- your issue seems to relate to that overload selection thing, identified by @jcalz at #12424 (comment). @atrauzzi: just tried, seems heterogeneous map isn't working yet. I'll check it out. |
BTW, I'm just curious about ambiguity when we mix expressions and types. What would be here? type F = () => number;
const F: () => string;
type TypeOfF = typeof (F()); // is F expression or type? |
@Igorbek: after changing
|
Ah, I see. So the syntax is type F = () => number;
const F: () => string;
type TypeOfF = F(); // number So, you're not trying to use |
Right. I'd talked a bit about my considerations on that in #6606 (comment) -- I just saw this as a simpler way to look at it than figuring out how functions/arguments stored in types would have cleanly fit into the earlier interpretations. 6606 evidently split into distinct proposals, and if people like both, then why not. Earlier snippet on this from #6606 (comment):
By the way, thank you for your thread on the overload resolution at #17471 -- I fear the major use-cases of this PR will have to defer to that, as they mostly depend on that overload pattern matching... Worse yet, despite most of the simple tests working, it looks like none of the useful applications of this are working yet. Function composition doesn't, heterogeneous |
Fixes the third part of microsoft#5453, needed to effectively type `bind` (Function.prototype), `curry` and `compose`. Depends on microsoft#17961 type calls, microsoft#17884 type spread and microsoft#18004 tuple spread. The first 2/3 are included, so changes will look more cluttered here though new changes just span 10 lines over 2 functions. Will properly test this when those are ready -- now still broken.
Further debugged a bit, seems composition might be suffering from the same evaluation order issue that overloads are, that #17471 mentioned earlier. I still need to check what the problem for |
You may want to add #1213 to the list of things that could be closed as a result of this bug. (Instead of "higher order types", I pass generic type-level functions instead.) There are potential concerns about type inference (it requires generalization before substitution rather than the usual other way around), but I'm not sure if that 1. is a real issue, and 2. if it would block the closure of that bug. |
I'm very excited to see the TypeScript interfaces created for fantasyland by @isiahmeadows based on these changes! I'm not sure I understand the "generalization before substitution" concern mentioned above. Also, maybe HKT get added to TS later. What I do know though, is that a lot of people don't have an opportunity to use this type of abstraction today because there aren't many strongly-typed languages that support it that are commonly used in larger orgs. I'd like to cheer on these efforts because I feel this is a great opportunity for a lot of professionals to really start to understand and use these powerful concepts. Typescript is a great platform for this because of its ubiquity. I'm continually impressed by how robust its type system is. |
Normally, TypeScript is used to doing things like this: interface Foo<T> { ... }
declare function foo<T>(value: T): Foo<T> In this case, you can infer the generic type just by the value itself. Now, consider this type: interface Functor<T extends <A>(x: A) => Functor<T, A>, A> { ... }
declare function foo<T extends <A>(x: A) => Functor<T, A>, A>(value: T(A)): Functor<T, A> In this case, you have to infer the generic type function from the value itself first, so you have to work backwards. (This is the Here, it has to first infer Normally, this extra generalization step isn't required, but it could show up in other existing, more obscure scenarios, hence why I'm not sure if it'll be an issue: interface Foo<T extends (x: any) => any, A extends (T extends (x: infer A) => any ? A : never)> { ... }
declare function foo<
T extends (x: any) => any,
A extends (T extends (x: infer A) => any ? A : never)
>(value: T): Foo<T, A> I fixed an issue with my types where it wasn't carrying the type info correctly. Note that the fix requires you to always pass the concrete "nested" type around as well, thanks to the lack of simple existentials within interface types. |
@isiahmeadows is the point here to cut down on boilerplate in e.g. gcanti's approach, or was that not working well yet? That said, looks like your approach is using type call syntax for backward inference. I hadn't done anything for that. I'm not sure there's an easy way to do it though. Like, for a given JS result value and input parameters, could you determine "the" function that would yield that result for those given values? I believe there would not be one correct answer. We're talking about the type equivalent of that scenario, but yeah. |
Kind of, but that particular approach isn't correct with subtypes. Mine still requires you specify the type function, but it's correct provided all types are given (which was my goal).
This specifically was my concern: I wasn't sure if the function could be inferred from its return value + arguments. It's possible to add it as a special case, but generalizing that is how you get (a subset of) inductive reasoning, and of course, we both know that's a hard problem. (I'm constantly impressed with what Haskell can infer.)
Of course. But the easier problem to solve would be "what is the most general function that 1. matches the constraint and 2. yields that result for those given values", and that's the one users would expect. This conveniently happens to be the type specified in the constraint, for reasons I presume you're already aware of. |
I'm abit lost here. How do you know that the supplied value actually is an instance of functor? To check that the supplied value is assignable to |
@isiahmeadows @jack-williams: |
You need to know |
@tycho01 I was slightly off about the inference rules - it only works when all invocations of interface Functor<T extends <A>(x: A) => Functor<T, A>, A> {
map<B>(this: T(A), f: (x: A) => B): T(B)
} The inductive step here would be generating interface Foo<T> { ... }
type Mapper = <A>(x: A) => A extends {foo: any} ? Foo<A["foo"]> : Foo<A>
declare function choose<T extends Mapper, A, B>(foo: T(A), bar: T(B)): T(A | B) In this scenario, given Side note: a variant of |
@isiahmeadows: well, even if we'd figure out how to make it (any languages featuring reference implementations?), even the trivial empty tuple PR has been sitting stale here. |
@tycho01 I'm not sure whether I could do an equivalent typing in Flow, but I'll try once I have time again. Edit: clarity |
@isiahmeadows: seems Gulio tried HKTs and static land in Flow. |
Thanks for your contribution. This PR has not been updated in a while and cannot be automatically merged at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to continue working on this PR, please leave a message and one of the maintainers can reopen it. |
@tycho01 What's the status of this? |
As much as I'd love to see this, it doesn't look likely to get approval from the team based on @mhegazy 's most recent comment on the matter: #6606 (comment) |
@treybrisbane I'm also coming under the impression that this isn't exactly the best way to handle higher-order types, particularly within TypeScript itself. It's hackish, and it doesn't fully address the issues it needs to solve. |
@isiahmeadows: in another PR @sandersn explained to me the issues with AST rewrites. In this PR, those issues were pretty much on a different level: iirc when your type call would error, the error squiggles would show up in a different place (the 'function' rather than the call?). I still believe a feature like this would be a break-through for the language.
Would you mind elaborating on that? Perhaps understanding this will help pave a way forward. |
First, it requires inductive generalization, something TypeScript lacks altogether. Second, declarations and non-inferred usages for common types like IMHO something that integrates with polymorphic |
Are some of the original issues this work tries to achieve fixed by Tuples in rest parameters and spread expressions in v3? |
@Meligy Not really. This is addressing a completely independent set of issues, such as conditional types, higher order types, and mapping over tuples/etc. One of those has already been implemented separately, but using this for the second is a major hack that doesn't even fully work most of the time and the third can only be partially implemented via this + a bunch of arcane type magic, really. |
@isiahmeadows: yeah, I think you're right about polymorphic
While my tuple PoC did use recursion, fwiw object |
@tycho01 But how well does it work with tuples? |
@isiahmeadows We just merged a change that makes mapped types map over tuple elements. |
Okay, then never mind. Thanks! 👍 |
See the tests for usage. Syntax mirrors JS function calls, though expression-level variables must be lifted to the type level with
typeof
as before.Progress toward fixing #6606 and others:
typeof
variables, generics, interfaces, function literals, what have you()()()
if desiredmap
reduce
makechooseOverload
consider type parameter values, not just their constraints, see Mapped conditional types #12424 (comment) / Allow calls to overloaded functions when all possible combinations of union type parameters resolve to valid overloads #17471awaited
use-case from Adds 'awaited' type to better handle promise resolution and await #17077I took to heart the advice not to make the implementation too crazy -- although I added a type call node to parse this type variant of the traditional call expression, in the checker I opted to just 'cheat' and get it to evaluate
Fn(A, B)
as expression(null! as Fn)(null! as A, null! as B)
.@Igorbek may now laugh at me for mocking
null! as type
notation.Note: the ambiguousEdit: I've removed type argument lists on type calls as they don't seem very useful.Ref<A>()
is currently parsed such that the type argument list<A>
belongs to the type reference, not to the function call. I've enabledRef<><A>()
as an alternative. Before, adding an empty type argument list<>
to a non-generic type would error stating the type is not generic.