-
Notifications
You must be signed in to change notification settings - Fork 12
suggestion: dynamic function type #8
Comments
Maybe through hlists? export type THListToFunction<L extends THList, C> = {
true: () => C
false: {
true: (a: THListHead<L>) => C
false: {
true: (a: THListHead<L>, b: THListHead<THListTail<L>>) => C
false: {
true: (a: THListHead<L>, b: THListHead<THListTail<L>>, c: THListHead<THListTail<THListTail<L>>>) => C
false: 'error' // TODO more cases
}[IsZero<THListLength<THListTail<THListTail<THListTail<L>>>>>]
}[IsZero<THListLength<THListTail<THListTail<L>>>>]
}[IsZero<THListLength<THListTail<L>>>]
}[IsZero<THListLength<L>>]
export type THListToCurriedFunction<L extends THList, C> = {
true: () => C
false: {
true: (a: THListHead<L>) => C
false: {
true: (a: THListHead<L>) => (b: THListHead<THListTail<L>>) => C
false: {
true: (a: THListHead<L>) => (b: THListHead<THListTail<L>>) => (c: THListHead<THListTail<THListTail<L>>>) => C
false: 'error' // TODO more cases
}[IsZero<THListLength<THListTail<THListTail<THListTail<L>>>>>]
}[IsZero<THListLength<THListTail<THListTail<L>>>>]
}[IsZero<THListLength<THListTail<L>>>]
}[IsZero<THListLength<L>>] Usage import { THListToFunction, TupleLength, THListReverse, THListToCurriedFunction, TupleToTHList } from 'typelevel-ts'
type L = THListReverse<TupleToTHList<[number, string, boolean]>>
// type F = (a: number, b: string, c: boolean) => void
type F = THListToFunction<L, void>
// type C = (a: number) => (b: string) => (c: boolean) => void
type C = THListToCurriedFunction<L, void> |
in a nutshell: improvement, but still... basically same problems as with objects and tuples, I can't get the naive usage example to work. export type Func<A, R=void> = THListToFunction<THListReverse<TupleToTHList<A>>, R> so that I can use it like this: declare function parseInt(string: string, radix?: number): number;
const stringToNumber: Func<[string], number> = parseInt;
// $ExpectError Type 'number' is not assignable to type 'string'
const numberToNumber: Func<[number], number> = parseInt; The closest I got was: export type Func<L extends THList, R=void> = THListToFunction<THListReverse<L>, R> and I can use it like this: declare function parseInt(string: string, radix?: number): number;
const stringToNumber: Func<TupleToTHList<[string]>, number> = parseInt;
// $ExpectError Type 'number' is not assignable to type 'string'
const numberToNumber: Func<TupleToTHList<[number]>, number> = parseInt; I guess replacing the length with declare function parseInt(string: string, radix?: number): number;
const stringToNumber: Func<Args<[string]>, number> = parseInt;
// $ExpectError Type 'number' is not assignable to type 'string'
const numberToNumber: Func<Args<[number]>, number> = parseInt; |
YES export type Func<T, R=void, I = 0, L = THNil> = {
true: Func<T, R, Increment[I], THCons<T[I], L>>;
false: THListToFunction<THListReverse<L>, R>;
}[ObjectHasKey<T, I>]; |
@amir-arad nice! |
yeah... this is nice. |
the problem is with the way |
I would get rid of the export type Func<T, R=void, L = THNil> = {
true: Func2<T, Increment[0], R, THCons<T[0], L>>;
false: THListToFunction<THListReverse<L>, R>;
}[ObjectHasKey<T, 0>];
export type Func2<T, I, R=void, L = THNil> = {
true: Func2<T, Increment[I], R, THCons<T[I], L>>;
false: THListToFunction<THListReverse<L>, R>;
}[ObjectHasKey<T, I>]; |
Does TupleLength work for you guys? It doesn't seem to work for me. |
I think we're running into a compiler bug, where ObjectHasKey doesn't work on generic type parameters when used in an indexer or something like that. |
@gcnew Do you happen to know why this code fails? type A<T> = {
true: 'true'
false: 'false'
}[ObjectHasKey<T, '0'>]
type B = ObjectHasKey<[string], '0'> // B = "true"
type C = A<[string]> // C = "false" Do you think your PR fixes this one? |
Unfortunately no, I don't know what's going wrong and my PR doesn't fix it either. I'll try to take a second look later tonight. |
In
I'm not sure why this substitution is only selectively applied, though. |
To be honest
|
A very large part of the useful abstractions in TypeScript end up being hacky mapped types. Perhaps we should just petition the TypeScript team to formalize a few fundamental ones into proper features of the language. As for the topic of the issue, I feel like the feature is actually not that hard to add, the issue is just that some of the needed constructs are too hacky. I'll have a look to see if we can find alternate implementations that perform better. |
From microsoft/TypeScript#12351. I've not been completely right, though. The real cause is a bit different. Mapped types inside intersections are not recognised as such and the resulting type is eagerly computed. I've been experimenting in a branch and the above code works there as expected. However, indexing errors caused by wrong index signatures are not detected properly. E.g. the following passes unnoticed: type StringContains<S extends string, L extends string> = ({ [K in S]: 'true' } & {
[key: string]: 'lie' // notice `lie` here
})[L]
type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>
type A1<T> = {
true: 'true'
false: 'false'
}[ObjectHasKey<T, '1'>] // no error, but `lie` is invalid |
I've opened microsoft/TypeScript#17456 describing the intersection indexing issue. |
gladly microsoft/TypeScript#17456 is scheduled to typescript 2.5 |
Unfortunately, even with these fixes and the new spread/rest types landing in 2.5, I think it still won't give us proper support for currying, bind, call, etc. The issue is that if the function in question has generics, I don't see how any feature could ever make that work correctly. What we will get is a solution that solves most of the use cases, but not all Here's a list of issues that are relevant to this issue: All of these are marked for 2.5 release |
So based on whether we write Ironically, I'd actually consider that a feature, in the sense it'd solve another issue of otherwise not having a way to access the index type when using property access to a key part of object prototype, e.g. The good thing is we can mostly control whether we get intersection types (using that and ^ edit: oh, guess
I tried a curry PoC in 5453 given that and 6606 that properly handles dynamic return types.
Given that, I don't really see immediate solutions there that would actually be faithful to its expression-level functionality in providing a value for the For
Given 6606 you would be able to dynamically calculate the return types for given inputs and generics. Do you see further problems aside from the
Given object spread/rest are just sugar to
That'd suck pretty hard, I'm glad the index functions as a fallback instead of like that :), can't really do anything useful much with such intersections... |
regarding bind, curry etc. |
To explain, what this approach can't address is return types that depend on the input (6606), or, worse, depend on generics of On-topic, 17456 then this? export type Func<A extends any[], R=void> = {
0: ()=>R
1: (a0:A[0]) => R
2: (a0: A[0], a1: A[1]) => R
// ...
}[TupleLength<A>];
const stringToNumber: Func<[string], number> = Number.parseInt; //works
const numberToNumber: Func<[number], number> = Number.parseInt; // breaks |
could you explain what you mean by
? |
@amir-arad: this snippet also wouldn't work yet, since as you guys noted, |
@tycho01 export interface Lazy<R, T=never>{
(this:T):R;
bind<T1 extends T>(ctx:T1):BoundLazy<R, T1>; // BoundLazy is similar to lazy only with no bind method or something
// ...
} and then a generic Function type: export type Func<A, R=void, T=never> = { // I dont think A works the same as an array, it's a tuple.
0: Lazy<R, T>;
1: Function1<A[0], R, T>;
2: Function2<A[0], A[1], R, T>;
// ...
}[NumberToString[TupleLength<A>]]; again, newbie's naive approach, would welcome feedback : ) |
I misjudged; looks like I guess that means that handling function 6606 could simultaneously help make for accurate return types, 5453 would make for better syntax/performance. If 6606 were expanded on to allow specifying Provisional code snippets following those proposals: // unbind: in TS already automatically happens when it should in JS (getting a method without directly applying it).
export type Unbind<F extends (this: This, ...args: Args) => R, This, Args extends any[], R> = Fn<[This, ...Args], R>;
// ^ can't capture params in generic until maybe #5453, error "A rest parameter must be of an array type."
// ^ #6606 upgrade: don't capture `R`, instead use `F(this: This, ...Args)`
export type Unbind<F extends (this: This, t1: T1) => R, This, R, T1> = Fn<[This, T1], R>;
export type Unbind<F extends (this: This, t1: T1, t2: T2) => R, This, R, T1, T2> = Fn<[This, T1, T2], R>;
export type Unbind<F extends (this: This, t1: T1, t2: T2, t3: T3) => R, This, R, T1, T2, T3> = Fn<[This, T1, T2, T3], R>;
// ...
// ^ wait, discriminating generic `F` type probably needs #6606 overloads
// complication: most stdlib methods don't have the `this` param specified. fix that. wonder why it isn't added implicitly...
export type Bind<F extends (this: This, ...args: Args) => R, This, Args, R, T extends This> = Fn<Args, R>;
// ^ doesn't handle other params yet. `unbind` notes above apply as well.
// ^ accurate return type over `R`: `F(this: T, ...Args)`.
Well, technically you can re-bind. Otherwise, essentially, yeah. I suppose it'd be nicer to solve
I now see you're addressing You're definitely adding something that |
While trying to give a good use case for advanced |
With that compiler issue solved, I'd have expected |
I just retried on recent TS, and got tests for this |
Following gcanti/fp-ts#177,
It would be great if we had a variadic generic function type before microsoft/TypeScript#5453 is resolved.
My best shot was this:
(I've opened a separate PR #7 for
NumberToString
as it seems useful on its own)usage example:
However the first generic param,
I
seems redundant though I couldn't shake it off. I'm beginning to use this type in my own project, and I would welcome any improvement ideas.@SimonMeskens
The text was updated successfully, but these errors were encountered: