Skip to content
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

Support placeholders and selectable-overloads #173

Closed
ikatyang opened this issue May 8, 2017 · 87 comments
Closed

Support placeholders and selectable-overloads #173

ikatyang opened this issue May 8, 2017 · 87 comments

Comments

@ikatyang
Copy link
Member

ikatyang commented May 8, 2017

Ideas

If TS cannot select the correct signature, why don't we select it ourselves?

If types are too hard to build manually, why don't we generate it programmatically?

Types

consider the following types for R.adjust(), it looks ugly but works perfectly in my testing.

type PH = {'@@functional/placeholder': true};
declare const __: PH;

// adjust.d.ts
type adjust = adjust_000;
type adjust_000 = {
  <T, U>(fn: (v: T) => U, index: number, array: T[]): adjust_111<T, U>;
  <T, U>(fn: (v: T) => U, _index: PH, array: T[]): adjust_101<T, U>;
  <T>(_fn: PH, index: number, array: T[]): adjust_011<T>;
  <T>(_fn: PH, _index: PH, array: T[]): adjust_001<T>;
  <T, U>(fn: (v: T) => U, index: number): adjust_110<T, U>;
  (_fn: PH, index: number): adjust_010;
  <X extends "111">(): <T, U>(fn: (v: T) => U, index: number, array: T[]) => adjust_111<T, U>;
  <X extends "101">(): <T, U>(fn: (v: T) => U, _index: PH, array: T[]) => adjust_101<T, U>;
  <X extends "011">(): <T>(_fn: PH, index: number, array: T[]) => adjust_011<T>;
  <X extends "001">(): <T>(_fn: PH, _index: PH, array: T[]) => adjust_001<T>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, index: number) => adjust_110<T, U>;
  <X extends "01">(): (_fn: PH, index: number) => adjust_010;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => adjust_100<T, U>;
  <T, U>(fn: (v: T) => U): adjust_100<T, U>;
};
type adjust_001<T> = {
  <U>(fn: (v: T) => U, index: number): adjust_111<T, U>;
  (_fn: PH, index: number): adjust_011<T>;
  <X extends "11">(): <U>(fn: (v: T) => U, index: number) => adjust_111<T, U>;
  <X extends "01">(): (_fn: PH, index: number) => adjust_011<T>;
  <X extends "1">(): <U>(fn: (v: T) => U) => adjust_101<T, U>;
  <U>(fn: (v: T) => U): adjust_101<T, U>;
};
type adjust_010 = {
  <T, U>(fn: (v: T) => U, array: T[]): adjust_111<T, U>;
  <T>(_fn: PH, array: T[]): adjust_011<T>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, array: T[]) => adjust_111<T, U>;
  <X extends "01">(): <T>(_fn: PH, array: T[]) => adjust_011<T>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => adjust_110<T, U>;
  <T, U>(fn: (v: T) => U): adjust_110<T, U>;
};
type adjust_011<T> = {
  <U>(fn: (v: T) => U): adjust_111<T, U>;
};
type adjust_100<T, U> = {
  (index: number, array: T[]): adjust_111<T, U>;
  (_index: PH, array: T[]): adjust_101<T, U>;
  <X extends "11">(): (index: number, array: T[]) => adjust_111<T, U>;
  <X extends "01">(): (_index: PH, array: T[]) => adjust_101<T, U>;
  <X extends "1">(): (index: number) => adjust_110<T, U>;
  (index: number): adjust_110<T, U>;
};
type adjust_101<T, U> = {
  (index: number): adjust_111<T, U>;
};
type adjust_110<T, U> = {
  (array: T[]): adjust_111<T, U>;
};
type adjust_111<T, U> = (T | U)[];

Since curried function can return itself by calling with non-parameter, we can use generic to select one of those overloads.

If we want to choose one of the overloads in the signature, it just works fine. ( the "11" means choosing the one with 2 ("11".length) parameters and both parameter are not placeholder )

ps. I think something like "11" is easily to notice which case to use, but it can be changed into something else.

R.adjust<"11">() //=> <T, U>(fn: (v: T) => U, index: number) => adjust_110<T, U>;

and with placeholder ( the "01" means choosing the one with 2 ("01".length) parameters, the first parameter is a placeholder, and the last parameter is not a placeholder )

R.adjust(R.__, 1)<"01">() //=> <T>(_fn: PH, array: T[]) => adjust_011<T>

If there are many kinds of the function, like R.map() for array, functor, etc., we can create them one by one and mixed them with intersection ( & ):

type map_array = map_array_00;
type map_functor = map_functor_00;
type map = map_array & map_functor;

If TS cannot find the correct signature, we can do it ourselves, it just works fine with good looking.

(R.map as R.map_array)(blah, blah, ...)

Implementation

To implement this types, I wrote a dts DOM library dts-element, the following code will generate the above adjust.d.ts.

import * as dts from 'dts-element';

const name = 'adjust';
const placeholder = new dts.BasicType({name: 'PH'});

const generic_T = new dts.GenericType({name: 'T'});
const generic_U = new dts.GenericType({name: 'U'});

// <T, U>(fn: (v: T) => U, index: number, array: T[]) => (T | U)[]
const function_type = new dts.FunctionType({
  generics: [generic_T, generic_U],
  parameters: [
    new dts.Parameter({
      name: 'fn',
      type: new dts.FunctionType({
        parameters: [
          new dts.Parameter({name: 'v', type: generic_T}),
        ],
        return: generic_U,
      }),
    }),
    new dts.Parameter({name: 'index', type: dts.number_type}),
    new dts.Parameter({name: 'array', type: new dts.ArrayType({owned: generic_T})}),
  ],
  return: new dts.ArrayType({owned: new dts.UnionType({types: [generic_T, generic_U]})}),
});

const types = dts.create_curried_function_types({
  name,
  placeholder,
  type: function_type,
  selectable: true,
});
const document = new dts.Document({children: types});

console.log(document.emit());

I am new to functional programming and ramda, I may not consider something else, please correct me if I am wrong.

I think functional programming is a better way to write code and hope to build a full-supported ramda.d.ts, let me know if you need any help, I am happy to make this types better and better.

@KiaraGrouwstra
Copy link
Member

KiaraGrouwstra commented May 9, 2017

Hey, thanks for making this thread.

You've touched upon both sources for typing variations per function: both actual variations (normally based on input types), and currying variations.

In that first category, your (R.map as R.map_array) example is pretty interesting -- you're right that with partial application TS has trouble selecting the right variation there, and if this is a way to solve that, then that sounds like a pretty welcome alternative to having to manually override the type otherwise.

I liked the idea of codegen to deal with currying as well, and though it wasn't fully finished, a current attempt at doing that did include code for adjust so far.
The first thing one would notice in your version is that your definition has ended up somewhat more convoluted than the current manual version.
My interpretation here is that this difference stems from the fact that your version is also able to deal with 0-parameter application, returning the function itself. Is that right?

Out of curiosity, would you mind commenting on the use-case / context where 0-param application became a point for you? I realize it's valid Ramda. That said, I do feel concerned about the utility vs. cost economics of this feature, both for the per-function code on our end (comparing the fragments required to generate the code), as well as for users (code without explicit flags still works, right?).

Overall, the topic you're bringing up is actually pretty interesting. I could, for example, imagine a different variation than you've mentioned so far: using e.g. R.map<'array'>()(fn)(arr), utilizing the "0-param application yields a free chance to select overload" idea you gave for currying.

On enabling 0-param application, I wonder if there may be an easier way, kind of like:

interface ZeroEnabled<F> { 
  () => F;
  F;
}

... or, well, any actually working simple-ish alternative in that fashion, whatever that might be...

@ikatyang
Copy link
Member Author

ikatyang commented May 9, 2017

usecase of selectable overloads

My interpretation here is that this difference stems from the fact that your version is also able to deal with 0-parameter application, returning the function itself. Is that right?

Yes, it will return itself but with the specified overload used only.

That said, I do feel concerned about the utility vs. cost economics of this feature, both for the per-function code on our end (comparing the fragments required to generate the code), as well as for users (code without explicit flags still works, right?).

So I put the selectable overloads before the last case, TS will select the last case while it cannot determine which to use, correct me if I am wrong.

type adjust_100<T, U> = {
  (index: number, array: T[]): adjust_111<T, U>;
  (_index: PH, array: T[]): adjust_101<T, U>;
  <X extends "11">(): (index: number, array: T[]) => adjust_111<T, U>;
  <X extends "01">(): (_index: PH, array: T[]) => adjust_101<T, U>;
  <X extends "1">(): (index: number) => adjust_110<T, U>; // before last case
  (index: number): adjust_110<T, U>; // TS will select the last case while cannot be determined
};

Actually, I have tried to build my own @ikatyang/types-ramda at the begining with just placeholders supported, but I had faced to a problem like:

declare function example<R>(fn: (...args: any[]) => R): R; 

while passing the curried function like adjust, it cannot pick up the overload I want to use.

example(R.adjust) //=> ???
example(R.adjust<"11">()) //=> predictable

So I started to create dts-element for generating the dts structurally.


readability for codegen vs manual

As you can see in the first comment, the following will generate the ugly types.

const types = dts.create_curried_function_types({
  name,
  placeholder,
  type: function_type,
  selectable: true,
});
const document = new dts.Document({children: types});
console.log(document.emit()); //=> type adjust_XXX ...

It is not just for generating curried types, it can generate all valid dts, the following will generate the original type

const type_declaration = new dts.TypeDeclaration({
  name,
  type: function_type,
});
const document = new dts.Document({children:[type_declaration]});
console.log(document.emit()); //=> type adjust = <T, U>(fn: (v: T) => U, index: number, array: T[]) => (T | U)[]

It is easily to switch between ugly-curried and original cases.

About ZeroEnabled

I thought that useless cases are invalid, like

  • 0-param

    R.adjust()()()()()()()()(...)
  • last-parameter-placeholder

    R.adjust(R.__)(R.__)(R.__)(R.__)(R.__)(...)

But I found that 0-param can be used to select the specified overload, which is awesome.
And I still thought 0-param is an invalid usecase, but for selecting overload is OK.

I think there is no easy way for ZeroEnabled, since the overload order is important in TS.

Edit

interface ZeroEnabled<F> { 
  (): F;
  F; // <--- invalid...
}

Edit
...the above usecase is invalid, generic type can not be used as InterfaceMember, 'F' is considered an identifier.
So it may not have easier way to do such things.


And also, the interface version with placeholder (also auto generated):

interface CurriedFunction3<T1, T2, T3, R> {
  (v1: T1, v2: T2, v3: T3): R;
  (v1: T1, _2: PH, v3: T3): CurriedFunction1<T2, R>;
  (_1: PH, v2: T2, v3: T3): CurriedFunction1<T1, R>;
  (_1: PH, _2: PH, v3: T3): CurriedFunction2<T1, T2, R>;
  (v1: T1, v2: T2): CurriedFunction1<T3, R>;
  (_1: PH, v2: T2): CurriedFunction2<T1, T3, R>;
  (v1: T1): CurriedFunction2<T2, T3, R>;
}

@KiaraGrouwstra
Copy link
Member

I'm sorry, I finally understood the problem you're tackling is not 0-param calls, but Ramda placeholder (R.__), earlier requested in #120. At that time I'd also been hoping to tackle that on the level of the CurriedFunction interfaces, as you've suggested just now. For those, feel free to send a PR already, the version you showed looks good to me.
Perhaps put the placeholder versions above the normal versions though -- if T2 includes the type of PH (if it's an object or any), we'd wanna make sure it won't mistake the placeholder for a regular parameter.

That said, I do feel concerned about the utility vs. cost economics of this feature, both for:

  • the per-function code on our end (comparing the fragments required to generate the code),

On second glance, I think the data structure you use for codegen isn't so difference from the one I'd added in scripts -- it looks more verbose, but I guess the idea is the same otherwise, so I guess those should be compatible. This should address the concern of verbosity for contributors to this repo.

  • as well as for users (code without explicit flags still works, right?).

So I put the selectable overloads before the last case, TS will select the last case while it cannot determine which to use, correct me if I am wrong.

Yeah, that sounds correct. So that addresses my concern about UX -- thanks.

I'm starting to get where you're going about using generics over overloads using placeholder type, given you're not immediately passing parameters. I suppose in the use-case where parameters had been passed, one would probably want to handle placeholders with overloads with placeholder type, so that no generics would need to be manually specified.

I guess with my idea of putting variations e.g. array as options passed through generics as well, we'd stumble on a different question though, of how these two would interact. That is, in what order might one pass these? I suppose variations like array might no longer need to be decided at the end if one is flipping argument order as well, which sounds like the answer is "either 'first', or 'figure out by case, though it'll be complicated'".

If there are many kinds of the function, like R.map() for array, functor, etc., we can create them one by one and mixed them with intersection (&)

Complication: curried variants need to be mixed into one and the same interface, or TS would ignore all but the first one. I haven't tested your approach, but it's something to keep in mind here. This particular point was currently handled in my codegen in scripts.
If they can be mixed even when separate like this though, that'd be great -- makes it easier to get separate versions to pre-select by those as you suggested (be it by cast or by generic).

the above usecase is invalid, generic type can not be used as InterfaceMember

Yeah, I know, just tried to give the simplest version I could think of. But now I realize enabling 0-param application wasn't really your goal, just a means to pre-selecting placeholder variations for use in un-applied functions.

I'm initially under the impression unapplied placeholders seem a bit of a niche (compared to applying them right away); otherwise I'm under the impression there are no real blockers here though, so if we could combine the advantages of our codegen methods (low verbosity, dealing with placeholders / currying / mixing partially applied variations), then that would be great.

@ikatyang
Copy link
Member Author

ikatyang commented May 9, 2017

Perhaps put the placeholder versions above the normal versions though if T2 includes the type of PH (if it's an object or any), we'd wanna make sure it won't mistake the placeholder for a regular parameter.

Did you mean the following code?

interface CurriedFunction3<T1, T2, T3, R> {
  (v1: T1, _2: PH, v3: T3): CurriedFunction1<T2, R>;
  (_1: PH, v2: T2, v3: T3): CurriedFunction1<T1, R>;
  (_1: PH, _2: PH, v3: T3): CurriedFunction2<T1, T2, R>; // above the normal version
  (v1: T1, v2: T2, v3: T3): R;
  (_1: PH, v2: T2): CurriedFunction2<T1, T3, R>; // above the normal version
  (v1: T1, v2: T2): CurriedFunction1<T3, R>;
  (v1: T1): CurriedFunction2<T2, T3, R>;
}

If so, it would be a strange UX but will work perfectly with T2 === any since TS will select the first matched overload, you'll see the placeholder version first instead of the detailed one.

UX vs Correctness, I think correctness is better.


On second glance, I think the data structure you use for codegen isn't so difference from the one I'd added in scripts -- it looks more verbose, but I guess the idea is the same otherwise, so I guess those should be compatible.

My version is more verbose since I have to track the dependencies of the generic types.

for example

append: [
  ['T', 'U'],
  {
    el: 'U',
    list: 'List<T>',
  },
  '(T & U)[]'
]

It looks good, but how can I trace the dependencies of T and U ?

el ( uses U ) and list ( uses T ) can be different order, we cannot use something like returns.indexOf('T') to track it.

This should address the concern of verbosity for contributors to this repo.

I know, but UX vs Correctness again. Maybe we can link this issue as a reference?

EDIT: Sorry, I forgot the regex /\b${xxx}\b/, but I think my structure is better on using #clone(), #has(), #equal(), etc., and dts full-supported.


I guess with my idea of putting variations e.g. array as options passed through generics as well, we'd stumble on a different question though, of how these two would interact. That is, in what order might one pass these? I suppose variations like array might no longer need to be decided at the end if one is flipping argument order as well, which sounds like the answer is "either 'first', or 'figure out by case, though it'll be complicated'".

Sorry, I just forgot this case.

Since I can generate it manually, so I think it can be generated programmatically, I'll try it later.

manual:

type map_11_array<U> = U[];
type map_11_mappable<U> = Mappable<U>;
type map_10_array<T, U> = {
  (array: T[]): map_11_array<U>;
};
type map_10_mappable<T, U> = {
  (mappable: Mappable<T>): map_11_mappable<U>;
};
type map_10<T, U> = {
  (array: T[]): map_11_array<U>;
  <K extends 'array'>(): map_10_array<T, U>;
  <K extends 'mappable'>(): map_10_mappable<T, U>;
  (mappable: Mappable<T>): map_11_mappable<U>;
};
type map_01_array<T> = {
  <U>(fn: (v: T) => U): map_11_array<U>
};
type map_01_mappable<T> = {
  <U>(fn: (v: T) => U): map_11_mappable<U>
};
type map_00_array = {
  <T, U>(fn: (v: T) => U, array: T[]): map_11_array<U>;
  <T>(_fn: PH, array: T[]): map_01_array<T>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, array: T[]) => map_11_array<U>;
  <X extends "01">(): <T>(_fn: PH, array: T[]) => map_01_array<T>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => map_10_array<T, U>
  <T, U>(fn: (v: T) => U): map_10_array<T, U>;
};
type map_00_mappable = {
  <T, U>(fn: (v: T) => U, mappable: Mappable<T>): map_11_mappable<U>;
  <T>(_fn: PH, mappable: Mappable<T>): map_01_mappable<T>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, mappable: Mappable<T>) => map_11_mappable<U>;
  <X extends "01">(): <T>(_fn: PH, mappable: Mappable<T>) => map_01_mappable<T>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => map_10_mappable<T, U>;
  <T, U>(fn: (v: T) => U): map_10_mappable<T, U>;
};
type map_00 = {
  <T, U>(fn: (v: T) => U, array: T[]): map_11_array<U>;
  <T, U>(fn: (v: T) => U, mappable: Mappable<T>): map_11_mappable<U>;
  <T>(_fn: PH, array: T[]): map_01_array<T>;
  <T>(_fn: PH, mappable: Mappable<T>): map_01_mappable<T>;
  <K extends 'array', X extends "11">(): <T, U>(fn: (v: T) => U, array: T[]) => map_11_array<U>;
  <K extends 'array', X extends "01">(): <T>(_fn: PH, array: T[]) => map_01_array<T>;
  <K extends 'array', X extends "1">(): <T, U>(fn: (v: T) => U) => map_10_array<T, U>
  <K extends 'array'>(): map_00_array;
  <K extends 'mappable', X extends "11">(): <T, U>(fn: (v: T) => U, mappable: Mappable<T>) => map_11_mappable<U>;
  <K extends 'mappable', X extends "01">(): <T>(_fn: PH, mappable: Mappable<T>) => map_01_mappable<T>;
  <K extends 'mappable', X extends "1">(): <T, U>(fn: (v: T) => U) => map_10_mappable<T, U>;
  <K extends 'mappable'>(): map_00_mappable;
  <T, U>(fn: (v: T) => U): map_10<T, U>;
};

Complication: curried variants need to be mixed into one and the same interface, or TS would ignore all but the first one. I haven't tested your approach, but it's something to keep in mind here. This particular point was currently handled in my codegen in scripts. If they can be mixed even when separate like this though, that'd be great -- makes it easier to get separate versions to pre-select by those as you suggested (be it by cast or by generic).

It seems it just mixin the first layer, for example:

type A = {
  (): {
    (): string;
  };
  (): number;
};

type B = {
  (): {
    (): symbol;
  };
  (): boolean;
};

type C = A & B;

declare const c: C;

c()() //=> string

@KiaraGrouwstra
Copy link
Member

CurriedFunction

Did you mean the following code?

Yeah, both CurriedFunction and the actual typings, yeah. Like, in this part the PH version should go first too in case of any / whatever:

  <T, U>(fn: (v: T) => U, index: number, array: T[]): adjust_111<T, U>;
  <T, U>(fn: (v: T) => U, _index: PH, array: T[]): adjust_101<T, U>;

how can I trace the dependencies of T and U?

I ran into exactly that in the current implementation. I just check when we get to new parameters (using a non-placeholder, of course) -- if these contain (= case-sensitive full-word regex match) one of the generics not used thus far, then now it's used, so should be added.

It seems it just mixin the first layer

Hm, yeah, looks like merging typings using just & wouldn't get what we'd need for TS. That said, we're currently already able to generate both mixed and separate versions (mixed versions are generated when there are multiple options, i.e. for add, separate versions is what it generates when there are no additional options, like for append), so I guess the hardest bits there have been tackled already...

I've now added what's generated by the current codegen so as to show current progress -- the intent being for it to replace the manually maintained file somehow (wouldn't mind a copy-paste, just wanted for contributors not to have to deal with the currying).

@ikatyang
Copy link
Member Author

ikatyang commented May 10, 2017

i.e. for add

Isn't it and ?!

EDIT: for R.and(), I'd suggest the signature like

function and<T, U>(a: T, b: U): T | U;

since the source code was just a && b.

I always look at the source code and think about what function signature should be, maybe I should record all the signature of R.XXX() first?


I've now added what's generated by the current codegen so as to show current progress -- the intent being for it to replace the manually maintained file somehow (wouldn't mind a copy-paste, just wanted for contributors not to have to deal with the currying).

I'd suggest making some scripts for copy-paste automatically, since copy-paste manually may make some mistakes.


I've done my codegen for merged version and fix the problem of ordering, but I think there may be some bugs since I haven't tested it in every case.

various

type map = map_00;
type map_00 = {
  <T>(_fn: PH, mappable: Mappable<T>): map_mappable_01<T>;
  <T>(_fn: PH, array: T[]): map_array_01<T>;
  <T, U>(fn: (v: T) => U, array: T[]): map_array_11<T, U>;
  <T, U>(fn: (v: T) => U, mappable: Mappable<T>): map_mappable_11<T, U>;
  <K extends "array", X extends "01">(): <T>(_fn: PH, array: T[]) => map_array_01<T>;
  <K extends "mappable">(): map_mappable_00;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => map_10<T, U>;
  <K extends "array">(): map_array_00;
  <K extends "array", X extends "11">(): <T, U>(fn: (v: T) => U, array: T[]) => map_array_11<T, U>;
  <K extends "mappable", X extends "11">(): <T, U>(fn: (v: T) => U, mappable: Mappable<T>) => map_mappable_11<T, U>;
  <K extends "mappable", X extends "01">(): <T>(_fn: PH, mappable: Mappable<T>) => map_mappable_01<T>;
  <T, U>(fn: (v: T) => U): map_10<T, U>;
};
type map_10 = {
  (array: T[]): map_array_11<T, U>;
  <K extends "array">(): (array: T[]) => map_array_11<T, U>;
  <K extends "mappable">(): (mappable: Mappable<T>) => map_mappable_11<T, U>;
  (mappable: Mappable<T>): map_mappable_11<T, U>;
};
type map_array_00 = {
  <T>(_fn: PH, array: T[]): map_array_01<T>;
  <T, U>(fn: (v: T) => U, array: T[]): map_array_11<T, U>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, array: T[]) => map_array_11<T, U>;
  <X extends "01">(): <T>(_fn: PH, array: T[]) => map_array_01<T>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => map_array_10<T, U>;
  <T, U>(fn: (v: T) => U): map_array_10<T, U>;
};
type map_array_01<T> = {
  <U>(fn: (v: T) => U): map_array_11<T, U>;
};
type map_array_10<T, U> = {
  (array: T[]): map_array_11<T, U>;
};
type map_array_11<T, U> = U[];
type map_mappable_00 = {
  <T>(_fn: PH, mappable: Mappable<T>): map_mappable_01<T>;
  <T, U>(fn: (v: T) => U, mappable: Mappable<T>): map_mappable_11<T, U>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, mappable: Mappable<T>) => map_mappable_11<T, U>;
  <X extends "01">(): <T>(_fn: PH, mappable: Mappable<T>) => map_mappable_01<T>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => map_mappable_10<T, U>;
  <T, U>(fn: (v: T) => U): map_mappable_10<T, U>;
};
type map_mappable_01<T> = {
  <U>(fn: (v: T) => U): map_mappable_11<T, U>;
};
type map_mappable_10<T, U> = {
  (mappable: Mappable<T>): map_mappable_11<T, U>;
};
type map_mappable_11<T, U> = Mappable<U>;

normal

type adjust = adjust_000;
type adjust_000 = {
  <T, U>(fn: (v: T) => U, _index: PH, array: T[]): adjust_101<T, U>;
  <T>(_fn: PH, index: number, array: T[]): adjust_011<T>;
  <T>(_fn: PH, _index: PH, array: T[]): adjust_001<T>;
  <T, U>(fn: (v: T) => U, index: number, array: T[]): adjust_111<T, U>;
  (_fn: PH, index: number): adjust_010;
  <T, U>(fn: (v: T) => U, index: number): adjust_110<T, U>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => adjust_100<T, U>;
  <X extends "101">(): <T, U>(fn: (v: T) => U, _index: PH, array: T[]) => adjust_101<T, U>;
  <X extends "011">(): <T>(_fn: PH, index: number, array: T[]) => adjust_011<T>;
  <X extends "001">(): <T>(_fn: PH, _index: PH, array: T[]) => adjust_001<T>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, index: number) => adjust_110<T, U>;
  <X extends "01">(): (_fn: PH, index: number) => adjust_010;
  <X extends "111">(): <T, U>(fn: (v: T) => U, index: number, array: T[]) => adjust_111<T, U>;
  <T, U>(fn: (v: T) => U): adjust_100<T, U>;
};
type adjust_001<T> = {
  (_fn: PH, index: number): adjust_011<T>;
  <U>(fn: (v: T) => U, index: number): adjust_111<T, U>;
  <X extends "11">(): <U>(fn: (v: T) => U, index: number) => adjust_111<T, U>;
  <X extends "01">(): (_fn: PH, index: number) => adjust_011<T>;
  <X extends "1">(): <U>(fn: (v: T) => U) => adjust_101<T, U>;
  <U>(fn: (v: T) => U): adjust_101<T, U>;
};
type adjust_010 = {
  <T>(_fn: PH, array: T[]): adjust_011<T>;
  <T, U>(fn: (v: T) => U, array: T[]): adjust_111<T, U>;
  <X extends "11">(): <T, U>(fn: (v: T) => U, array: T[]) => adjust_111<T, U>;
  <X extends "01">(): <T>(_fn: PH, array: T[]) => adjust_011<T>;
  <X extends "1">(): <T, U>(fn: (v: T) => U) => adjust_110<T, U>;
  <T, U>(fn: (v: T) => U): adjust_110<T, U>;
};
type adjust_011<T> = {
  <U>(fn: (v: T) => U): adjust_111<T, U>;
};
type adjust_100<T, U> = {
  (_index: PH, array: T[]): adjust_101<T, U>;
  (index: number, array: T[]): adjust_111<T, U>;
  <X extends "11">(): (index: number, array: T[]) => adjust_111<T, U>;
  <X extends "01">(): (_index: PH, array: T[]) => adjust_101<T, U>;
  <X extends "1">(): (index: number) => adjust_110<T, U>;
  (index: number): adjust_110<T, U>;
};
type adjust_101<T, U> = {
  (index: number): adjust_111<T, U>;
};
type adjust_110<T, U> = {
  (array: T[]): adjust_111<T, U>;
};
type adjust_111<T, U> = (T | U)[];

interfaces

interface CurriedFunction1<T1, R> {
  (v1: T1): R;
}
interface CurriedFunction2<T1, T2, R> {
  (_1: PH, v2: T2): CurriedFunction1<T1, R>;
  (v1: T1, v2: T2): R;
  (v1: T1): CurriedFunction1<T2, R>;
}
interface CurriedFunction3<T1, T2, T3, R> {
  (v1: T1, _2: PH, v3: T3): CurriedFunction1<T2, R>;
  (_1: PH, v2: T2, v3: T3): CurriedFunction1<T1, R>;
  (_1: PH, _2: PH, v3: T3): CurriedFunction2<T1, T2, R>;
  (v1: T1, v2: T2, v3: T3): R;
  (_1: PH, v2: T2): CurriedFunction2<T1, T3, R>;
  (v1: T1, v2: T2): CurriedFunction1<T3, R>;
  (v1: T1): CurriedFunction2<T2, T3, R>;
}

How can I do for this repo now? It is difficult for me to write the scripts (FP) here, since I am new to ramda and FP ( I just read the docs and haven't used it... ). Should I generate the dts using my version and make some PR or .. ?

@KiaraGrouwstra
Copy link
Member

Isn't it and ?!

Both add and and have multiple, yeah. Just like map you used in your example just now. :)

I'd suggest making some scripts for copy-paste automatically

Yeah, fair enough.

How can I do for this repo now?

If you've already generated the CurriedFunction interfaces, feel free to update them here.

For the full typings, before I could merge a PR I'm thinking we'd probably need to finish the generated typings over here first so that we could test for the differences between them. I don't have much time for open-source recently though, so I realize that isn't helping much...

@ikatyang
Copy link
Member Author

Both add and and have multiple, yeah. Just like map you used in your example just now. :)

add: {
  base: [
    ['T extends {and?: Function}'], // <--- I thought this is not for add...
    {
      fn1: 'T',
      val2: 'boolean+any'
    },
    'boolean'
  ],
  no_generics: [
    [],
    {
      v1: 'any',
      v2: 'any',
    },
    'boolean',
  ]
},

If you've already generated the CurriedFunction interfaces, feel free to update them here.
For the full typings, before I could merge a PR I'm thinking we'd probably need to finish the generated typings over here first so that we could test for the differences between them. I don't have much time for open-source recently though, so I realize that isn't helping much...

I thought that just updating the CurriedFunction interfaces does not make sense..

I'll try the full typings myself in my repo first, and we can mix them together or do something else while you have time to do such work :)

@KiaraGrouwstra
Copy link
Member

I thought this is not for add...

Ouch, crap, thanks for noticing! Fixed now.

I thought that just updating the CurriedFunction interfaces does not make sense..

I know it'd only add partial support, but I'd think it'd still help a bit already, no? Anyway, no strong preference here -- as you prefer.

I'll try the full typings myself in my repo first, and we can mix them together or do something else while you have time to do such work :)

Fair enough, sorry for the trouble!

P.S.: Saw you're from Taiwan; pretty cool place. :D

@ikatyang
Copy link
Member Author

I know it'd only add partial support, but I'd think it'd still help a bit already, no? Anyway, no strong preference here -- as you prefer.

Would you mind the CurriedFunction 1~9 to be 1000+ rows...

image


P.S.: Saw you're from Taiwan; pretty cool place. :D

😊

@KiaraGrouwstra
Copy link
Member

Would you mind the CurriedFunction 1~9 to be 1000+ rows...

On that one, well...

I used to have similar codegen for the path function, trying to give it proper inference, which also ended up super long for 7~9. When I tried it, I could no longer compile my application anymore -- the path definition made it too slow. So then I just got rid of the longer segments.

I think a similar strategy could work well here. Fortunately, I think Ramda probably doesn't even really have many functions that have many parameters... so I imagine we could probably get away with ignoring the longer ones.

@ikatyang
Copy link
Member Author

ikatyang commented May 10, 2017

Curried1: 1 rows
Curried2: 3 rows
Curried3: 7 rows
Curried4: 15 rows
Curried5: 31 rows
Curried6: 63 rows
Curried7: 127 rows
Curried8: 255 rows
Curried9: 511 rows

I prefer 1 ~ 7 or 1 ~ 8, what do you think?

@KiaraGrouwstra
Copy link
Member

My initial reflex would be toward 3-6 -- I feel a bit hesitant toward 7-9, since versions over 3 don't seem likely to be used much in practice (only in curry and invoker), while in my experience a big number of overloads could significantly affect compilation times.

That could become a concern for other typings with placeholder support as well. It'd be unfortunate if non-users would fall victim to that, so perhaps that could merit separate branches. Please feel free to experiment. Hopefully things will be fine.

@ikatyang
Copy link
Member Author

ikatyang commented Jun 3, 2017

I just rewrote my whole codegen library dts-element for v2, it is available to parse dts by using TS API now, so that my codegen is easily to understand now, and it will be restructured into this.

And I wrote a snapshot tester dts-jest base on jest and typings-checker, so that it will be easily to test with watching mode.

And I realize that there are some strange(?) typings in this repo that I can't just copy all of them, such as R.and, there is no andable parameters in the source file of Ramda.

Additionaly, Ramda v0.24.1 had just released.

Here's my progress now, any suggestion?

@KiaraGrouwstra
Copy link
Member

Whoops, seems you're right the dispatching for R.and was no longer there anymore. I ditched that now. Feel free to point out / sent PRs for others!

Thanks for noting the Ramda update as well; I've now opened #181 for that.

In #165 @whitecolor had also pointed out to me that it'd be of importance to add run-time testing so as to ensure run-time / compile-time alignment. And that's fair. Could you perhaps comment a bit on how you see dts-jest positioned?

That said, the typings-checker fork used now has had its motivations as well (see there + #123) -- preventing any inferences, and a version-controlled diff log until tests pass. In practice, this means string-based exact-match testing for types, and using code snippets over line numbers in the error output.

@whitecolor: since you had also suggested a new code-gen approach there, could you weigh in as well (and maybe vice-versa)? Hopefully we'll end up with the best of three worlds.

To try and summarize considerations contributed / emphasized:

@whitecolor (taken verbatim from #165):

  • Make typings code base and tests more consistent and well structured
  • Ensure all functions are typed and tests
  • Allow to use/import ramda functions individually

Also covered:

  • comments
  • code-gen in TS over JS
  • don't rely on JS objects for order

@ikatyang:

  • support placeholders (R.__)
  • selectable overloads (for placeholders, typing variants like array/object in R.map)

Others I'm making out from the code (could you comment on these?)

  • sorting typings
  • abstracting out parameter names
  • comments, incl. JSDoc

@tycho01:

  • reduce per-typing code for maintainability and reduce learning curve for contributors
  • generating mixed interface for curried variants, needed cuz TS ignores interface below the first that matches
  • automatic deference of generic declaration to the first time they're required. complication: if only used by another generic (e.g. with keyof) instead of in the type signature itself (curried away to later invocation), inference fails.
  • consideration for performance, in my path() experience shown to drop with dozens of 100+ overloads

Some comments I had on @whitecolor's approach:

  • the desire to also split output into separate files convoluted the code-gen to handle imports; I proposed addressing this by keeping source separate, output together.
  • enable import merge from 'ramda/src/merge' by flipping it around and having those reference the big file, instead of the big file reference the smaller files?

Some questions on yours:

  • How does the sorting match TS's order from specific to general, minus placeholders as you mentioned?
  • abstracting out param names and structures into function calls (e.g. ${_.List('T')})): both seems a bit more verbose this way than the original strings; were these to facilitate mass renaming? On param names, I guess Ramda has these in their codebase, even if not mentioned in docs, but yeah, I'm not so worried about names.

Here's my progress now, any suggestion?

On getting the other 90% of the Ramda typings covered in a scalable manner, might it be viable to parse from these so you could focus efforts could be focused on the universal logic (be it from this starting point, the universal utils you already had down, or anything in between)? I hope that would help make placeholder support a somewhat less colossal task... Feel free to comment.

That said, I'd suggest also still trying to check performance when used in projects, to ensure you wouldn't run into issues like the one I had with path before.

Hopefully in the future TS would get powerful enough that we could do dynamic compile-time calculation to handle complexity (typing variants, currying, placeholders) rather than having massive overloads per function typing, but it's not quite there yet...

@ikatyang
Copy link
Member Author

ikatyang commented Jun 4, 2017

In #165 @whitecolor had also pointed out to me that it'd be of importance to add run-time testing so as to ensure run-time / compile-time alignment. And that's fair.

I used to think that I just have to check its type, but after seeing #165 I realized that in some cases may cause problems, just like R.and() above. I agree with adding real tests.

Could you perhaps comment a bit on how you see dts-jest positioned? That said, the typings-checker fork used now has had its motivations as well (see there + #123) -- preventing any inferences, and a version-controlled diff log until tests pass. In practice, this means string-based exact-match testing for types, and using code snippets over line numbers in the error output.

The reason I created dts-jest is that it is more comfortable for users to test, since jest has its powerful CLI interaction and watch mode, see Jest: Immersive Watch Mode.

In dts-jest, you can use :show flag at first to see what type is it, and so as to choose snapshot it or not, see also dts-jest testing, that said if any appeared, you'll notice that might be a wrong type.

For the version-controlled diff log, you can use :show flag unless the type is correct, or manually adjust the snapshot so as to see the difference between them.

And yes, there is also code snippets with line numbers in the output feature in dts-jest.

Additionally, I think the testing in this repo now is somehow weird, I can't understand why tests/test.ts: 450 / 725 checks passed. will pass the test in travis, isn't that pass = all correct ?

Others I'm making out from the code (could you comment on these?)

  • sorting typings
  • abstracting out parameter names
  • comments, incl. JSDoc
  • sorting typings

    The sorting function is here, currently I just sort with the following rules:

    • placeholder version go first
    • normal typings go fisrt
    • selectable typings will be inserted before the last typings
  • abstracting out parameter names

    I'm not sure what name to use so I choose to make them referenced so as to refactor easily, and I want to have the consistent naming with checking for length of generic.

  • comments, incl. JSDoc

    I'm a big fan for using Go to Definition to see the detail information, so that with JSDoc I can get the fully infomrations, and I used to see somewhere that VSCode will support more kind of JSDoc tag in the future.

How does the sorting match TS's order from specific to general, minus placeholders as you mentioned?

I think TS is smart to choose correct typings, and if it can't, just use the selectable overload.

abstracting out param names and structures into function calls (e.g. ${_.List('T')})): both seems a bit more verbose this way than the original strings; were these to facilitate mass renaming? On param names, I guess Ramda has these in their codebase, even if not mentioned in docs, but yeah, I'm not so worried about names.

Well, if you have the standard naming and does not mind to have consistent naming that I have said above, I can just follow that and remove the abstract naming.

On getting the other 90% of the Ramda typings covered in a scalable manner, might it be viable to parse from these so you could focus efforts could be focused on the universal logic (be it from this starting point, the universal utils you already had down, or anything in between)? I hope that would help make placeholder support a somewhat less colossal task... Feel free to comment.

I'll try it later, thanks for pointing out.

That said, I'd suggest also still trying to check performance when used in projects, to ensure you wouldn't run into issues like the one I had with path before.

I have no idea what problem you had faced to path before, but I think my version seems flat(?) enough to reference between each others. And of course, if it will affect performance, I think we can just make some version like normal, with placeholder, with selectable-overloads, fully-typed or something else, they can just select which to use basd on their RAM(?).

Hopefully in the future TS would get powerful enough that we could do dynamic compile-time calculation to handle complexity (typing variants, currying, placeholders) rather than having massive overloads per function typing, but it's not quite there yet...

Yes, I hope so too, but they are still in the Future roadmap, hope to see it in some version's release note someday.

@KiaraGrouwstra
Copy link
Member

Yeah Travis is still broken. Thanks for elaborating on the other points. The Jest snapshots look pretty interesting!

On overload order w.r.t. placeholders, I'm under the impression we may still have a problem:

var placeholder: { placeholder: true };
var any: any;
var object: object;

declare function foo(v: { placeholder: true }): 0;
declare function foo(v: object): 1;

var a = foo(placeholder); // 0
var b = foo(any); // 0 :(
var c = foo(object); // 1

declare function bar(v: object): 1;
declare function bar(v: { placeholder: true }): 0;

var d = bar(placeholder); // 1 :(
var e = bar(any); // 1
var f = bar(object); // 1

With path, basically, I had hundreds of overloads as well, and tests worked fine, but actually compiling a mobile project using it suddenly didn't really work anymore until I cut out on most of the overloads there. That one was also flat, at least. But yeah, if it turns out to have a bad impact then offering options in different branches or the like seems a fair solution there.

I also eagerly await the horizon of the TS roadmap. :)

@ikatyang
Copy link
Member Author

ikatyang commented Jun 4, 2017

On overload order w.r.t. placeholders, I'm under the impression we may still have a problem

Yes, it is. I think that is the pain point we cannot resolve it without selectable overloads, since one is OK then another will be not OK. We can only choose one to be the general case.

type PH = { placeholder: true };

declare const placeholder: PH;
declare const any: any;
declare const object: object;

declare const foo: {
  (v: PH): 0;
  <X extends "0">(): (v: PH) => 0;
  <X extends "1">(): (v: object) => 1;
  (v: object): 1;
}

var a = foo(placeholder); // 0
var b = foo<"1">()(any); // 1 :)
var c = foo(object); // 1

Summary

  1. add real tests
  2. use standard naming instead of abstract naming
  3. migrate those exist typings in this repo
  4. check those exist typings with Ramda source code
  5. support Ramda v0.24.x
  6. and something I haven't realized?

Let me know if this is correct or not :)

@KiaraGrouwstra
Copy link
Member

  1. add real tests

I think tests here had originally been based on those in the Ramda repo (+ currying, type/error assertions), though not kept in sync at least -- I haven't looked at the original tests, for one. I guess they were still missing run-time assertions though, yeah.

  1. use standard naming instead of abstract naming
  2. migrate those exist typings in this repo

I guess with 3, 2 should be covered. I guess for the purpose of handling placeholder support we could probably put off the refactor though?

I guess what I'm trying to get at with that is I like the flexibility of the current notation, with separated data/logic.
It'd just allow switching from say my codegen to your codegen, and support e.g. normal, with placeholder, with selectable-overloads, fully-typed modes, etc., with 0 per-function chores.
I think that aspect is pretty powerful of not putting functions into the data. I guess control kinda gets inverted.

  1. check those exist typings with Ramda source code

Well, I suppose if we have run-time tests, then ones for like R.and dispatching would probably error fast, notifying us of the changes. I think they've mostly been cutting down on dispatching anyway, so in that sense things have gotten less complicated. It looks like placeholders have been spared so far though! 😄

  1. support Ramda v0.24.x

Ideally, though leaving it to its own separate issue is fine as well.

  1. and something I haven't realized?

That's already a lot of points primarily just for placeholder support. :)

@ikatyang
Copy link
Member Author

ikatyang commented Jun 23, 2017

I've almost done my types-ramda, so I think it's time to get some feedback.

Done

use standard naming instead of abstract naming

See templates/map.d.ts -> src/map.d.ts and templates/README.md for example.

migrate those exist typings in this repo
check those exist typings with Ramda source code

I have looked every typings and migrate them one by one, and rewrite some of them if necessary.

There are several wrong types in this repo and even wrong test cases, such as Lens.

support Ramda v0.24.x

I've also done the new types for v0.23.0...v0.24.1, and some of them have been removed by Ramda, such as R.isArrayLike.

Not Yet

add real tests done
multi-version typings done

I haven't done this yet, but it seems not hard to do.

DefinitelyTyped compatible

It is easy to compile my own exist tests for DT compatible, but I have no idea if I should do so, since it will be a lot of breaking changes for placeholder support.


Let me know what you think about this. And if you think it is time to send a fully rewrite PR to this repo, I am happy to do so.

EDIT: There are several functions that I have no idea how to integration test them, since they are all ramda-fantasy types needed, e.g. R.pipeK, R.composeK, R.sequence and R.traverse.

@KiaraGrouwstra
Copy link
Member

KiaraGrouwstra commented Jun 26, 2017

Tests:

Using the markdown files to take care of the comments looks awesome! That definitely makes for a much better editing experience than the previous ideas there like manual comments / strings.

It is easy to compile my own exist tests for DT compatible, but I have no idea if I should do so, since it will be a lot of breaking changes for placeholder support.

The .d.ts templates are looking fairly manageable as well. I suppose it might be possible to further expand on the current idea by allowing overload selection based on just either selector or kind, but obviously you're already ahead in this respect anyway.

As to tests, I feel like swapping them out in one go is a bit unfortunate in the sense it makes it harder to compare the situation before and after this PR. Might it be possible to run the current test suite on it as well so as to compare output? I realize we can make things DT-compatible by commenting anything not working well yet, but yeah, that's a separate concern I guess.

Honestly I love how you lifted Ramda's JSDoc comments to get top-level support, and I wish we could similarly just provide an eq typing with type-level type checks so we could just directly use Ramda's own tests to verify the typings as well.
Unfortunately there are multiple obstacles there:

  • type-level type checks aren't possible yet (TS's 6606)
  • we'd need further power to do value-level prediction (TS's 5453)
  • currying currently requires more testing for the typings (for implicit overload selection and late info for e.g. keyof)
  • verifying equality of function types (might work by default?)
  • verifying intended presence of type errors (can't use tsc anyway, could maybe ditch them)
  • the same values could be passed to the functions in TS using different type representations, which can and will affect inference quality

So basically that's a consideration after 6606+5453 land.

Code-gen:

On separation of concerns with data vs. logic, it's interesting you picked a different path by manipulating typings directly rather than storing them as POJOs.

I definitely see the beauty of your path there, as obviously any abstracting representation can be considered opinionated, while you've managed to pick a data representation that, for the purpose of abstracting over currying and overload selection, manages not to diverge from TS, which helps reduce learning curve for the functions that don't require further code-gen.

I think for the purpose of the .c.ts files though, the targeted representation is slightly sub-optimal now compared to what they could look like if we were instead generating a POJO (/AST?) representation there. For the purpose of directly generating the TS representation, that seems to get a bunch of cruft from the TS representation with things like .join(',').

I guess conversion from such an abstracted representation could be centralized -- that way the .c.ts cruft could just be handled in one place instead of everywhere separately. Note this is still compatible with the option to just directly use .d.ts typings like in your current distinction.

That said, although I liked the compactness of POJOs and the ease with which they'd enable different code-gen approaches, I guess I've been feeling much more strongly about overcoming the need for codegen to make TS usable than about the specific representations used...

On another note, it'd be cool to bootstrap the thing and eat our own dog-food by using generated typings to do the typing generation with Ramda in TS (might cut down on deps compared to lodash if those even get downloaded by users in the first place?), but yeah, not like Ramda itself looks pretty inside. ☺

tl;dr

If we can test to verify semantics aren't adversely affected, and ideally get some anecdotal evidence compilation isn't horribly suffering from the added placeholder support even for non-users, then let's merge this into master here.

it will be a lot of breaking changes for placeholder support

Could you elaborate on that?

On Ramda-Fantasy typings, yeah, might need to import some other library for testing I guess. Or just get the minimum type definition / implementation needed to test them, like some Maybe, I dunno.

@ikatyang
Copy link
Member Author

I suppose it might be possible to further expand on the current idea by allowing overload selection based on just either selector or kind, but obviously you're already ahead in this respect anyway.

I used to think about selectable-overloads with just $SEL or $KIND, it is possible but it will cause more and more overload types and bad consistency, for example:

type map_00 = {
    <T>(_fn: PH, list: List<T>): map_list_01<T>;
    <T>(_fn: PH, functor: Functor<T>): map_functor_01<T>;
    <T, U>(fn: Morphism<T, U>, list: List<T>): map_list_11<T, U>;
    <T, U>(fn: Morphism<T, U>, functor: Functor<T>): map_functor_11<T, U>;
    <$SEL extends "11", $KIND extends "list">(): <T, U>(fn: Morphism<T, U>, list: List<T>) => map_list_11<T, U>;
    <$SEL extends "11", $KIND extends "functor">(): <T, U>(fn: Morphism<T, U>, functor: Functor<T>) => map_functor_11<T, U>;
    <$SEL extends "01", $KIND extends "list">(): <T>(_fn: PH, list: List<T>) => map_list_01<T>;
    <$SEL extends "01", $KIND extends "functor">(): <T>(_fn: PH, functor: Functor<T>) => map_functor_01<T>;
    <$SEL extends "1">(): <T, U>(fn: Morphism<T, U>) => map_10<T, U>;
+    <$KIND extends "list">(): {
+      <T>(_fn: PH, list: List<T>): map_list_01<T>;
+      <T, U>(fn: Morphism<T, U>, list: List<T>): map_list_11<T, U>;
+      <$SEL extends "01">(): <T>(_fn: PH, list: List<T>) => map_list_01<T>;
+      <$SEL extends "11">(): <T, U>(fn: Morphism<T, U>, list: List<T>) => map_list_11<T, U>;
+      <$SEL extends "1">(): (list: List<T>) => map_list_11<T, U>;
+      (list: List<T>): map_list_11<T, U>;
+    };
+    <$KIND extends "functor">(): {
+      <T>(_fn: PH, functor: Functor<T>): map_functor_01<T>;
+      <T, U>(fn: Morphism<T, U>, functor: Functor<T>): map_functor_11<T, U>;
+      <$SEL extends "01">(): <T>(_fn: PH, functor: Functor<T>) => map_functor_01<T>;
+      <$SEL extends "11">(): <T, U>(fn: Morphism<T, U>, functor: Functor<T>) => map_functor_11<T, U>;
+      <$SEL extends "1">(): (functor: Functor<T>) => map_functor_11<T, U>;
+      (functor: Functor<T>): map_functor_11<T, U>;
+    };
+    <$SEL extends "11">(): {
+      <T, U>(fn: Morphism<T, U>, list: List<T>) => map_list_11<T, U>;
+      <$KIND extends "list">(): <T, U>(fn: Morphism<T, U>, list: List<T>) => map_list_11<T, U>;
+      <$KIND extends "functor">(): <T, U>(fn: Morphism<T, U>, functor: Functor<T>) => map_functor_11<T, U>;
+      <T, U>(fn: Morphism<T, U>, functor: Functor<T>) => map_functor_11<T, U>;
+    };
+    <$SEL extends "01">(): {
+      <U>(fn: Morphism<T, U>): map_list_11<T, U>;
+      <$KIND extends "list">(): <U>(fn: Morphism<T, U>) => map_list_11<T, U>;
+      <$KIND extends "functor">(): <U>(fn: Morphism<T, U>) => map_functor_11<T, U>;
+      <U>(fn: Morphism<T, U>): map_functor_11<T, U>;
+    }
    <T, U>(fn: Morphism<T, U>): map_10<T, U>;
};
type map_10<T, U> = {
    (list: List<T>): map_list_11<T, U>;
-    <$SEL extends "1", $KIND extends "list">(): (list: List<T>) => map_list_11<T, U>;
-    <$SEL extends "1", $KIND extends "functor">(): (functor: Functor<T>) => map_functor_11<T, U>;
+    <$KIND extends "list">(): (list: List<T>) => map_list_11<T, U>;
+    <$KIND extends "functor">(): (functor: Functor<T>) => map_functor_11<T, U>;
    (functor: Functor<T>): map_functor_11<T, U>;
};
type map_list_01<T> = {
    <U>(fn: Morphism<T, U>): map_list_11<T, U>;
};
type map_functor_01<T> = {
    <U>(fn: Morphism<T, U>): map_functor_11<T, U>;
};
type map_list_11<T, U> = U[];
type map_functor_11<T, U> = Functor<U>;

So I decided to use the minimal case for the performance and consistency, that is, $SEL is always the first type param, $KIND is the second type param which might be optional based on their conflict between kinds.


As to tests, I feel like swapping them out in one go is a bit unfortunate in the sense it makes it harder to compare the situation before and after this PR. Might it be possible to run the current test suite on it as well so as to compare output?

The root cause why my integration test ramda-tests.ts is all-in-one file, is that TS had to parse a lot of ramda/src/*.d.ts declarations before type-checking, so that separate file will cause bad performance on dev time ( 4~6 sec per test file on my PC, and as you can see my unit test time on Travis CI, it was terrible ).

And you can see the difference affects the types before/after the PR in the snapshot files, e.g. ikatyang/types-ramda@91c817a.


Honestly I love how you lifted Ramda's JSDoc comments to get top-level support, and I wish we could similarly just provide an eq typing with type-level type checks so we could just directly use Ramda's own tests to verify the typings as well.

There are some internal/helper functions in Ramda tests, so I think it might be hard to use their tests directly, but we can follow their release note to modify for the new/removed features after new version released.

Unfortunately there are multiple obstacles there:

  • type-level type checks aren't possible yet (TS's 6606)

The typeof for expression is not supported yet, but the programmatic-level typeof for expression is supported, that's how our typings-checker and dts-jest works.

  • we'd need further power to do value-level prediction (TS's 5453)

Let's wait for that, I think it is the most important feature for FP.

  • currying currently requires more testing for the typings (for implicit overload selection and late info for e.g. keyof)

That's the pain point I see every time, TS will infer their types based on their "order", that is, the following two cases will cause different results:

// base type
declare function sort<T>(fn: Comparator<T, number>, list: List<T>): T[];
const byAge = R.ascend(R.prop('age'));

R.sort(byAge, people); //=> T === any

R.sort(R.__, people)(byAge); //=> T === Person

Unfortunately, this is the advantage ( FP currying, target goes last ) and also the disadvantage ( the most specific goes last, while TS uses the first matched type to infer ), we have to ensure both side is compatible.

It can easy fix something like keyof vs Record, but it is impossible to fix something like the above example unless setting the type parameter explicitly.

  • verifying equality of function types (might work by default?)

Is that you mean the difference between function expression and function declaration? If so, they are the same thing on the type-level.

  • verifying intended presence of type errors (can't use tsc anyway, could maybe ditch them)

Is that you mean the failing tests? Currently I just set them as :fail to ensure they throw errors, and for others that expected passed but failed, I just set them as :skip, and you can set them as :show to see their type.

  • the same values could be passed to the functions in TS using different type representations, which can and will affect inference quality

Yes, but I can't figure out what case will be, so I think this kind of issue should be resolved after someone reported.


I think for the purpose of the .c.ts files though, the targeted representation is slightly sub-optimal now compared to what they could look like if we were instead generating a POJO (/AST?) representation there. For the purpose of directly generating the TS representation, that seems to get a bunch of cruft from the TS representation with things like .join(',').

It seems that the representation like your script.js is more reasonable, I'll improve those *.c.ts later.

I guess conversion from such an abstracted representation could be centralized -- that way the .c.ts cruft could just be handled in one place instead of everywhere separately. Note this is still compatible with the option to just directly use .d.ts typings like in your current distinction.

I think we can place their repeated helper function to ./utils, and then still write same format but more readable code there? Just like your script.js.

That said, although I liked the compactness of POJOs and the ease with which they'd enable different code-gen approaches, I guess I've been feeling much more strongly about enabling value-level prediction in TS to overcome the need for codegen to make TS usable than about the specific representations used...

Yes, it is. But it seems FP is not on their 1st level priority.

On another note, it'd be cool to bootstrap the thing and eat our own dog-food by using generated typings to do the typing generation with Ramda in TS (might cut down on deps compared to lodash if those even get downloaded by users in the first place?), but yeah, not like Ramda itself looks pretty inside. ☺

I'm afraid that might cause some problem due to our multi-version typings, and currently it was github-based installation so that I have to put template/generated code together, which I think it'd better to gitignore those generated types. It seems multi-branch is better: master for dev, generated-full-typed, generated-normal, etc. for installation.

And yes, using Ramda to coding is always my goal, I'll switch to Ramda once I confirm that it won't cause problems.


If we can test to verify semantics aren't adversely affected, and ideally get some anecdotal evidence compilation isn't horribly suffering from the added placeholder support even for non-users, then let's merge this into master here.

Yes, but I think TS's emitted declaration file will be terrible if someone want to export curried function directly...


it will be a lot of breaking changes for placeholder support. Could you elaborate on that?

I'm not sure about this, but I think it might be, since DefinitelyTyped and @types are separated for a long time, thus the order of overloads probably different, and I noticed that someone add some additional types to DT, e.g. CurriedTypeGuard, which is not exist in our repo.


On Ramda-Fantasy typings, yeah, might need to import some other library for testing I guess. Or just get the minimum type definition / implementation needed to test them, like some Maybe, I dunno.

I used to write types for Maybe, but it is somehow hard to do, since there are so many mixin and fully es5-based, that is, a lot of prototype assignment. I'll try it again later.


Summary

  • improve *.c.ts
  • use Ramda for codegen if possible
  • write ramda-fantasy types for testing

It seems very close to our goal now. :)

@KiaraGrouwstra
Copy link
Member

breaking changes

Hm, right. I'm personally not super worried about the bits of collateral damage if it's still creating a significant net gain, but yeah, I'm flexible there.

TS's emitted declaration file will be terrible if someone want to export curried function directly...

What would be your take on that?

R.sort(R.__, people)(byAge); //=> T === Person

I think most of these cases are caused by typings using keyof, and could be solved by falling back to typings not using that for cases where this problem kicks in. I think fallback typings we've already had for most of those cases.

Detecting the conditions (keyof used in the declaration of a generic needed for a provided param, with its target being a generic depending on info not yet available) and dropping the failing typing, to ensure it'll fall back to a working version, is one thing I think we can do as part of the codegen (see here.

That said, I think this covers cases such as R.path, for which a potential nicer alternative might not require keyof (see here). For many other cases it may need 5453 though.

As to tests, I feel like swapping them out in one go is a bit unfortunate in the sense it makes it harder to compare the situation before and after this PR. Might it be possible to run the current test suite on it as well so as to compare output?

Sorry, I may have worded this poorly.

I'm not so much concerned about whether they're in one/multiple files, but rather that the new code-base has not been judged using the old tests, which makes it harder to tell whether any new issues might have popped up so far. For that purpose, it should be useful to try running the old test suite on your generated typings as well.

You don't have to refactor them, but if running them on the new typings would take a multiple of the original compile time (since you're noting it has been taking time for you with a merged test file on your end already), in that case it may be a consideration to reserve a dedicated branch for placeholders.

@ikatyang
Copy link
Member Author

TS's emitted declaration file will be terrible if someone want to export curried function directly... What would be your take on that?

Sorry, I used to think the emitted declarations will be reflected ( emit whole CurriedFunction typings ), but it seems it'll just emit something like:

(some-emitted.d.ts)

export declare const emitted: CurriedFunction2<string, number, boolean>;

I think this kind of output is OK.


R.sort(R.__, people)(byAge); //=> T === Person
I think most of these cases are caused by typings using keyof, and could be solved by falling back to typings not using that for cases where this problem kicks in. I think fallback typings we've already had for most of those cases.
Detecting the conditions (keyof used in the declaration of a generic needed for a provided param, with its target being a generic depending on info not yet available) and dropping the failing typing, to ensure it'll fall back to a working version, is one thing I think we can do as part of the codegen (see here.
That said, I think this covers cases such as R.path, for which a potential nicer alternative might not require keyof (see here). For many other cases it may need 5453 though.

  1. filter those unusable types, but it seems works fine even not removed, just have to ensure keyof goes first and then Record, and yes, we have to handle those more complex cases.
function prop<T>(_key: PH, object: T): <K extends keyof T>(key: K) => T[K];
function prop<T, K extends keyof T>(key: K, object: T): T[K];
-function prop<T, K extends keyof T>(key: K): (object: T) => T[K];
-function prop<K extends string, T extends Record<K, any>>(_key: PH, object: T): (key: K) => T[K];
-function prop<K extends string, T extends Record<K, any>>(key: K, object: T): T[K];
function prop<K extends string>(key: K): <T extends Record<K, any>>(object: T) => T[K];
  1. generic that used more than once: make generics as late as possible (suppose the latest is the most specific).
-declare function sort<T>(_fn: PH, list: List<T>): (fn: Comparator<T, number>) => T[];
-declare function sort<T>(fn: Comparator<T, number>, list: List<T>): T[];
-declare function sort<T>(fn: Comparator<T, number>): (list: List<T>) => T[];

+declare function sort<T>(_fn: PH, list: List<T>): <U extends T>(fn: Comparator<U, number>) => T[];
+declare function sort<T, U extends T>(fn: Comparator<U, number>, list: List<T>): T[];
+declare function sort<U>(fn: Comparator<U, number>): <T extends U>(list: List<T>) => T[];

-R.sort(byAge, people); //=> T === any
+R.sort(byAge, people); //=> T === Person

R.sort(R.__, people)(byAge); //=> T === Person
  1. I'm not good at those type-level operation, so I may just pick up those types you mentioned. ( R.path only? )

I'm not so much concerned about whether they're in one/multiple files, but rather that the new code-base has not been judged using the old tests, which makes it harder to tell whether any new issues might have popped up so far. For that purpose, it should be useful to try running the old test suite on your generated typings as well.

There is the old test, I just copy-paste and normalize/lint them so as to match my pattern.

Additionally, I removed those issue-cases due to their incompleteness, some of them even does not have $ExpectType, I have no idea what it want to check for.

I haven't read those issues yet, so I may have to read them and make those test suites be in my tests.

You don't have to refactor them, but if running them on the new typings would take a multiple of the original compile time (since you're noting it has been taking time for you with a merged test file on your end already), in that case it may be a consideration to reserve a dedicated branch for placeholders.

There is a watching-mode build script in my repo, so I'm not worried about compile time, it's fast enough. And I think we only have to test the full-typed version, and those different version will just work fine.

The most time consuming part is testing, it costs 4~6 sec per file, but it is OK since I'll only run the ramda-tests.ts and those changed type's unit test. ( NOTE that jest run tests parallelly )

I mean that if I use Ramda in my codegen, I have to make a local copy of Ramda definitions so as to run those code without errors just like TS does ( TS compile itself using its previous version ).

I think I have some idea for that. :)

@KiaraGrouwstra
Copy link
Member

I'm not good at those type-level operation, so I may just pick up those types you mentioned. ( R.path only? )

Yeah, I'd gladly help out on anything in that area. Probably not many others who have been looking into that much anyway, haha.
It's still kinda in WIP though; I dunno why that path version fails when used as a function yet, and currently we're limited to arrays for input (only thing we can iterate over, think I can hack up object iteration with 5453) and anything except constructed arrays for output (can't manipulate those yet until 5453), making R.path one of the rare few functions (path, pathOr, mergeAll, fromPairs, zipObject) fitting the criteria of what we might be able to do now. So a bunch more is imaginable but we still have a few roadblocks.

There is the old test, I just copy-paste and normalize/lint them so as to match my pattern.

Awesome; saw tests for map differed and figured they might not be in yet. Might you have an output log for that so we could compare?

Additionally, I removed those issue-cases due to their incompleteness, some of them even does not have $ExpectType, I have no idea what it want to check for.

Yeah, I'd been too lazy to look into those w.r.t. types; I guess we should be able to learn their expected types by checking run-time output.

The most time consuming part is testing

Compilation part or run-time part?
Like, if we're only changing typings, then I imagine we could get by not re-running the JS tests, since they'd be guaranteed unchanged. In that sense I guess time for the JS ones isn't much of a concern, especially since it doesn't imply longer compilation times for users.

@ikatyang
Copy link
Member Author

Awesome; saw tests for map differed and figured they might not be in yet. Might you have an output log for that so we could compare?

I can't figure out what you mean about map, since I completely migrate them, and just remove those duplicate cases ( there are so many duplicates.. ) and those weird cases ( commented // /* */, no $ExpectTypeed, etc. ), that's it, I haven't remove other cases.

And I fixed those error types by using selectable-overloads, and then mark them as :pass (snapshoted), their inferred type match the original $ExpectType value, what I mean match is that they are equivalent but may represent in difference form, e.g. Dictionary<number> vs Record<string, number> ).

@types/npm-ramda's map test
ikatyang/types-ramda's map test


Compilation part or run-time part?

It's runtime part (TS parses those generated .d.ts files so as to infer types).

Like, if we're only changing typings, then I imagine we could get by not re-running the JS tests, since they'd be guaranteed unchanged. In that sense I guess time for the JS ones isn't much of a concern, especially since it doesn't imply longer compilation times for users.

I always execute my build-watch script in the background (almost immediately compiled, you can try my npm run build-watch script and then modify templates/*.d.ts to see the performance), and so does the jest test (watching mode, npm run test -- --watch), so that jest will start testing changed files once it found its test files changed.

The actual tests and type tests are actually executed separately.

And actual-tests are super faster (maybe 5x) than type tests, so that even execute them simultaneously won't cause performance problem, it's just fast enough.

@KiaraGrouwstra
Copy link
Member

Sorry, I'd checked here, no prob then.

match the original $ExpectType value, what I mean match is that they are equivalent but may represent in difference form, e.g. Dictionary<number> vs Record<string, number>).

Does that mean it checks type A > B / B < A? I ask because the string-based checks (using :show?) you mentioned earlier seem one of few ways to prevent any inference from type-checking.

And I fixed those error types by using selectable-overloads, and then mark them as :pass (snapshoted)

I take it imperfect inference would be :pass-muted now, but do we know if there were any regressions? I imagine the only way to find out would be to compare error output for the same test suite?

@ikatyang
Copy link
Member Author

Does that mean it checks type A > B / B < A? I ask because the string-based checks (using :show?) you mentioned earlier seem one of few ways to prevent any inference from type-checking.

I checked their inferrence by using :show at the first time, so that its inferred type will console.log on the jest CLI so I can confirm that it is correct, and then I'll set that flag to :pass, which mean not only no errors, but also "snapshot" its inferred type in snap file, jest will throw error once the value has changed. (some screenshots)

For checking both A -> B and B -> A, I have checked all B -> A, but some of A -> B are not valid since I make some types more specific, for example:

A (Dictionary<number>) -> B (Record<'some-literal', number>)

I take it imperfect inference would be :pass-muted now, but do we know if there were any regressions? I imagine the only way to find out would be to compare error output for the same test suite?

It's just not in the same file now, so that we don't have to copy-paste the changed type manually, it'll be done by jest using --updateSnapshot.

before

  • inferred types are placed in ./ramda-tests.ts
  • expressions are placed in ./ramda-tests.ts

after

  • inferred types are placed in ./__snapshots__/ramda-tests.ts.snap
  • expressions are placed in ./ramda-tests.ts

@ikatyang
Copy link
Member Author

Sorry for the delay, I was tidying my repos and finally done.

For current types, is it OK to send a PR? or should we wait for TS? since I have no idea what should I do for those types that we can't solve now.

@KiaraGrouwstra
Copy link
Member

Is it that bad now? Cuz I'm not expecting much there soon. I only recall you mention that promised thread as something you'd considered waiting for, but considering the limited overlap that can't be it right?

@ikatyang
Copy link
Member Author

I'm currently use Promise<T> | T as a workaround for promised, so the pipeP-like functions are fine now.

The only thing I concerned is that the R.path type, it's blocked by the type Something = SomeOperation[NumberLiteral], so that it currently uses any as return type, which will be a regression in this repo if I send a PR and merged.

@KiaraGrouwstra
Copy link
Member

That isn't microsoft/TypeScript#15768 right?
If it is a regression, might it not be an option to use the earlier one for that?

@ikatyang
Copy link
Member Author

Yeah, it seems I have to make sure there is no regression, I'll copy them back or write a temporary generator for them anyway, and once I done I'll send a PR here.

@KiaraGrouwstra
Copy link
Member

alright, cool. 😃

@ikatyang
Copy link
Member Author

After seeing Sanctuary's TS definition, I think it'd be better to expose the "curryify" generator from types-ramda so that it can be reused everywhere to generate FP definitions, I think I'll create the dts-element-fp first, and then making the PR here.

@KiaraGrouwstra
Copy link
Member

KiaraGrouwstra commented Jul 31, 2017 via email

This was referenced Aug 2, 2017
@KiaraGrouwstra
Copy link
Member

On the JSDoc thing, I presume that still depends on there being something JS-like you're appending them to? If it could work even for just types I'd be interested if I could use it for my type repo as well...

@ikatyang
Copy link
Member Author

ikatyang commented Aug 3, 2017

Uh, what did you mean for the JSDoc thing?

@KiaraGrouwstra
Copy link
Member

I recall you'd gen JSDoc entries with md snippets. I was just curious if that might work for typical as well, though I guess Ramda typings are just that too.
If JSDoc really looks at more than just the comments, that binding is what you're doing here huh?

@ikatyang
Copy link
Member Author

ikatyang commented Aug 3, 2017

It just parse using dts-element, bind JSDoc then emit, for example:

import * as dts from 'dts-element';

const definition = `
    declare const x: number;
    declare const y: boolean;
`;

const jsdocs = ['jsdoc for x', 'jsdoc foy y'];

const top_level_element = dts.parse(definition);

top_level_element.members.forEach((member, index) => {
    member.jsdoc = jsdocs[index];
});

console.log(dts.emit(top_level_element)); /*=>
    /**
     * jsdoc for x
     * /
    declare const x: number;
    /**
     * jsdoc foy y
     * /
    declare const y: boolean;
*/

but notice that I haven't implemented parser for comment, so that comments in input will be ignored, only output available.

@KiaraGrouwstra
Copy link
Member

Thanks, interesting. I'll need to look into this. :)

@ikatyang
Copy link
Member Author

ikatyang commented Aug 6, 2017

I'm trying to use the path types in this repo, but I found that there are some unexpected behavior:

// in-based
declare function path<T1 extends string, T2 extends string, TResult>(path: [T1, T2], obj: {[K1 in T1]: {[K2 in T2]: TResult}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, TResult>(path: [T1, T2, T3], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: TResult}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, T4 extends string, TResult>(path: [T1, T2, T3, T4], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: {[K4 in T4]: TResult}}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, T4 extends string, T5 extends string, TResult>(path: [T1, T2, T3, T4, T5], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: {[K4 in T4]: {[K5 in T5]: TResult}}}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, T4 extends string, T5 extends string, T6 extends string, TResult>(path: [T1, T2, T3, T4, T5, T6], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: {[K4 in T4]: {[K5 in T5]: {[K6 in T6]: TResult}}}}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, T4 extends string, T5 extends string, T6 extends string, T7 extends string, TResult>(path: [T1, T2, T3, T4, T5, T6, T7], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: {[K4 in T4]: {[K5 in T5]: {[K6 in T6]: {[K7 in T7]: TResult}}}}}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, T4 extends string, T5 extends string, T6 extends string, T7 extends string, T8 extends string, TResult>(path: [T1, T2, T3, T4, T5, T6, T7, T8], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: {[K4 in T4]: {[K5 in T5]: {[K6 in T6]: {[K7 in T7]: {[K8 in T8]: TResult}}}}}}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, T4 extends string, T5 extends string, T6 extends string, T7 extends string, T8 extends string, T9 extends string, TResult>(path: [T1, T2, T3, T4, T5, T6, T7, T8, T9], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: {[K4 in T4]: {[K5 in T5]: {[K6 in T6]: {[K7 in T7]: {[K8 in T8]: {[K9 in T9]: TResult}}}}}}}}}): TResult;

// Record-based
declare function path<K1 extends string, K2 extends string, TResult>(path: [K1, K2], obj: Record<K1,Record<K2,TResult>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, TResult>(path: [K1, K2, K3], obj: Record<K1,Record<K2,Record<K3,TResult>>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, K4 extends string, TResult>(path: [K1, K2, K3, K4], obj: Record<K1,Record<K2,Record<K3,Record<K4,TResult>>>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, K4 extends string, K5 extends string, TResult>(path: [K1, K2, K3, K4, K5], obj: Record<K1,Record<K2,Record<K3,Record<K4,Record<K5,TResult>>>>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, K4 extends string, K5 extends string, K6 extends string, TResult>(path: [K1, K2, K3, K4, K5, K6], obj: Record<K1,Record<K2,Record<K3,Record<K4,Record<K5,Record<K6,TResult>>>>>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, K4 extends string, K5 extends string, K6 extends string, K7 extends string, TResult>(path: [K1, K2, K3, K4, K5, K6, K7], obj: Record<K1,Record<K2,Record<K3,Record<K4,Record<K5,Record<K6,Record<K7,TResult>>>>>>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, K4 extends string, K5 extends string, K6 extends string, K7 extends string, K8 extends string, TResult>(path: [K1, K2, K3, K4, K5, K6, K7, K8], obj: Record<K1,Record<K2,Record<K3,Record<K4,Record<K5,Record<K6,Record<K7,Record<K8,TResult>>>>>>>>): TResult;
declare function path<K1 extends string, K2 extends string, K3 extends string, K4 extends string, K5 extends string, K6 extends string, K7 extends string, K8 extends string, K9 extends string, TResult>(path: [K1, K2, K3, K4, K5, K6, K7, K8, K9], obj: Record<K1,Record<K2,Record<K3,Record<K4,Record<K5,Record<K6,Record<K7,Record<K8,Record<K9,TResult>>>>>>>>>): TResult;

// for each path length list all combinations of objects and homogeneous arrays... tuples not supported yet.

declare function path<T1 extends string, TResult>(path: [T1], obj: {[K1 in T1]: TResult}): TResult;
declare function path<T1 extends number, TResult>(path: [T1], obj: TResult[]): TResult;
declare function path<T1 extends string, T2 extends string, TResult>(path: [T1, T2], obj: {[K1 in T1]: {[K2 in T2]: TResult}}): TResult;
declare function path<T1 extends string, T2 extends number, TResult>(path: [T1, T2], obj: {[K1 in T1]: TResult[]}): TResult;
declare function path<T1 extends number, T2 extends string, TResult>(path: [T1, T2], obj: {[K2 in T2]: TResult}[]): TResult;
declare function path<T1 extends number, T2 extends number, TResult>(path: [T1, T2], obj: TResult[][]): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends string, TResult>(path: [T1, T2, T3], obj: {[K1 in T1]: {[K2 in T2]: {[K3 in T3]: TResult}}}): TResult;
declare function path<T1 extends string, T2 extends string, T3 extends number, TResult>(path: [T1, T2, T3], obj: {[K1 in T1]: {[K2 in T2]: TResult[]}}): TResult;
declare function path<T1 extends string, T2 extends number, T3 extends string, TResult>(path: [T1, T2, T3], obj: {[K1 in T1]: {[K3 in T3]: TResult}[]}): TResult;
declare function path<T1 extends string, T2 extends number, T3 extends number, TResult>(path: [T1, T2, T3], obj: {[K1 in T1]: TResult[][]}): TResult;
declare function path<T1 extends number, T2 extends string, T3 extends string, TResult>(path: [T1, T2, T3], obj: {[K2 in T2]: {[K3 in T3]: TResult}}[]): TResult;
declare function path<T1 extends number, T2 extends string, T3 extends number, TResult>(path: [T1, T2, T3], obj: {[K2 in T2]: TResult[]}[]): TResult;
declare function path<T1 extends number, T2 extends number, T3 extends string, TResult>(path: [T1, T2, T3], obj: {[K3 in T3]: TResult}[][]): TResult;
declare function path<T1 extends number, T2 extends number, T3 extends number, TResult>(path: [T1, T2, T3], obj: TResult[][][]): TResult;

declare function path<T>(path: any[], obj: {}): T | undefined;
declare function path(path: any[]): <T>(obj: {}) => T | undefined;

declare const a_1_b_2_c_3: { a: 1, b: 2, c: 3 };

const r = path(['a', 'b', 'c'], a_1_b_2_c_3);
// TS inferred `r` as `1 | 2 | 3`
// but it's actually `undefined`, should be inferred as something contains `undefined`

The types looks perfect in theory, but TS somehow infer it weird, should I still use these types?

(snapshot)

@KiaraGrouwstra
Copy link
Member

I believe ['a', 'b', 'c'] may be regarded as to be of type string[] or [string, string, string] or something. Could you try putting that on an explicitly typed variable as well like the other one?
I've been discussing solutions to this (not degenerating types unless necessary) in #16389 and #16656.

@ikatyang
Copy link
Member Author

ikatyang commented Aug 6, 2017

(playground)

declare const a_1_b_2_c_3: { a: 1, b: 2, c: 3 };
declare const a_b_c: ['a', 'b', 'c'];

const r = path(a_b_c, a_1_b_2_c_3);
// still `1 | 2 | 3`
// a_b_c is inferred as [string]

OK, it seems I should remove these two overloads, so that TS will choose the last overload T | undefined instead of the weird [string].

// declare function path<T1 extends string, TResult>(path: [T1], obj: {[K1 in T1]: TResult}): TResult;
// declare function path<T1 extends number, TResult>(path: [T1], obj: TResult[]): TResult;

(after)

const r = path(a_b_c, a_1_b_2_c_3);
//=> {} | undefined

@KiaraGrouwstra
Copy link
Member

KiaraGrouwstra commented Aug 6, 2017

I see.
So even if you reorder the overloads like 3-2-1 instead, the 3 (trinary?) overload fail as it's missing the nested objects.
Since length 3 tuples somehow match length 1 tuples that 1 overload ends up matching, as that much of the ins succeed.
T must then become "a" | "b" | "c" to match the actual contents of the tuple though, at which point the property access operation yields 1 | 2 | 3.

Ugh.
Honestly overload approaches are pretty awkward when you have to deal with different circumstances. path overloads were exploding way too hard already. Hope we could just get that.

Edit: to clarify, I don't see great solutions right now. Killing the overload feels a bit awkward in the sense we'd be killing like all of them to save... nothing would be left to save.

If anything, a 3-length tuple matching on a 1-length one appears behavior microsoft/TypeScript#6229 aimed to address. It's a good example of why their proposal may matter.

@ikatyang
Copy link
Member Author

ikatyang commented Aug 9, 2017

Final diff and changes, looks like there is no regression now (some of them require late-inference to get accurate type, but I think it's currently acceptable), once you confirmed I'll start to work on a PR.

@KiaraGrouwstra
Copy link
Member

regressions?:

  • until
  • into: "Argument of type ..."
  • prop / propOr "not assignable to ..."
  • where error

questions:

  • truncate: is the Record<"length", any> what your late-inference comment refers to?
  • mapIndexed regression: how come it could do without the late inference before?
  • pipeP: if we're outputting PromiseLike, maybe use for input too?
  • lenses: a few errors, guess we need to sync this up with their current workings (also just came up in masaeedu's issue), I had a quick glance at it but couldn't quite make it out yet
  • pathOr: yields value 2 but gives type {} | "N/A" -- might be problematic since afaik 2 as a primitive doesn't match {}. I'll agree it'd be unfortunate to give up on having something more granular than the any though. I'm still working on getting type-level crap to better work out, hope I could help out there within too long.

I might've accidentally repeated a few from before, feel free to ignore as appropriate. anyway, looking pretty much ready :), it's a great win even if we just merge it in right now.

@ikatyang
Copy link
Member Author

  • until

This was caused by R.flip and R.gt, use placeholder version can fix this problem.

R.gt //=> <T extends Ordered>(a: T, b: T) => boolean
// TS inferred T -> {} -> any
R.flip(R.gt) //=> CurriedFunction2<any, any, boolean>

R.until(R.flip(R.gt)(100), R.multiply(2))(1); //=> any
R.until(R.gt(R.__, 100), R.multiply(2))(1); //=> number

  • into: "Argument of type ..."

See ikatyang/types-ramda@5f73499#commitcomment-23207966


  • prop / propOr "not assignable to ..."

There is no such property in that case, should we allow that kind of things? I think it'd better to use as any for this kind of cases.


  • where error

This needs late-inference or specify T to be any.

(prototype)

function where<T>(
  spec: Dictionary<Predicate<T>>,
  object: Dictionary<T>,
): boolean;
const spec = { x: R.equals(2) };
R.where(spec); //=> T = number
R.where(spec)({x: 1, y: 'moo', z: true});
//            ^^^^^^^^^^^^^^^^^^^^^^^^^ [ts]
// Argument of type '{ x: number; y: string; z: boolean; }' is not assignable to parameter of type 'Dictionary<number>'.
//   Property 'y' is incompatible with index signature.
//     Type 'string' is not assignable to type 'number'.

  • truncate: is the Record<"length", any> what your late-inference comment refers to?

Yes, T is considered Record<'length', any> after R.propSatisfies, need late-inference to let T = string after truncate('12345').

(prototype)

function when<T, U>(
  pred: Predicate<T>,
  whenTrueFn: Morphism<T, U>,
  value: T,
): T | U;
const truncate = R.when(
  R.propSatisfies(R.flip(R.gt)(10), 'length'), //=> T = something that has 'length' property -> Record<'length', any>
  R.pipe(R.take(10), R.append('…'), R.join('')) //=> U = string
);
// @dts-jest $ExpectType string -> string | Record<"length", any>
//                                 ^ U      ^ T
truncate('12345');         // => '12345'

  • mapIndexed regression: how come it could do without the late inference before?

Uh, mapIndexed was removed in ramda 0.16.0, so there's no mapIndexed in my types, it just uses R.addIndex now, which require more information (late-inference or manual type parameter) to get accurate type.


  • pipeP: if we're outputting PromiseLike, maybe use for input too?

Not sure what you mean, currently both input and output are PromiseLike.


  • lenses: a few errors, guess we need to sync this up with their current workings (also just came up in masaeedu's issue), I had a quick glance at it but couldn't quite make it out yet

I think I should add some pseudo-definition for lensProp and lensIndex to get accurate type, currently I'm just use kind of ManualLen to define every lens, which is usually useless (always inferred T to be {}).


  • pathOr: yields value 2 but gives type {} | "N/A" -- might be problematic since afaik 2 as a primitive doesn't match {}.

Uh, number is assignable to {}, {} means everything except null and undefined, I think what you mean "doesnt't match" is for object type.

@KiaraGrouwstra
Copy link
Member

Thanks for your response.

So where is potentially too strict now, for restricting the parameters to homogeneous objects.
I realize we can't really do a proper heterogeneous check yet, and a compromise would make the stricter homogeneous version useless.
Until we can do better though, I guess it's better to be a bit less strict about it if that's what it takes to cut out the false positive though.

Anyway, looking forward to the PR. :)

@ikatyang
Copy link
Member Author

Final final diff with two changes (ikatyang/types-ramda#98, ikatyang/types-ramda#97), but it seems the workaround for 15768 not work (just suppress the error and output any)

(playground)

type NumberToString = { 0:'0',1:'1',2:'2',3:'3',4:'4',5:'5',6:'6',7:'7',8:'8',9:'9',10:'10',11:'11',12:'12',13:'13',14:'14',15:'15',16:'16',17:'17',18:'18',19:'19',20:'20',21:'21',22:'22',23:'23',24:'24',25:'25',26:'26',27:'27',28:'28',29:'29',30:'30',31:'31',32:'32',33:'33',34:'34',35:'35',36:'36',37:'37',38:'38',39:'39',40:'40',41:'41',42:'42',43:'43',44:'44',45:'45',46:'46',47:'47',48:'48',49:'49',50:'50',51:'51',52:'52',53:'53',54:'54',55:'55',56:'56',57:'57',58:'58',59:'59',60:'60',61:'61',62:'62',63:'63',64:'64',65:'65',66:'66',67:'67',68:'68',69:'69',70:'70',71:'71',72:'72',73:'73',74:'74',75:'75',76:'76',77:'77',78:'78',79:'79',80:'80',81:'81',82:'82',83:'83',84:'84',85:'85',86:'86',87:'87',88:'88',89:'89',90:'90',91:'91',92:'92',93:'93',94:'94',95:'95',96:'96',97:'97',98:'98',99:'99',100:'100',101:'101',102:'102',103:'103',104:'104',105:'105',106:'106',107:'107',108:'108',109:'109',110:'110',111:'111',112:'112',113:'113',114:'114',115:'115',116:'116',117:'117',118:'118',119:'119',120:'120',121:'121',122:'122',123:'123',124:'124',125:'125',126:'126',127:'127',128:'128',129:'129',130:'130',131:'131',132:'132',133:'133',134:'134',135:'135',136:'136',137:'137',138:'138',139:'139',140:'140',141:'141',142:'142',143:'143',144:'144',145:'145',146:'146',147:'147',148:'148',149:'149',150:'150',151:'151',152:'152',153:'153',154:'154',155:'155',156:'156',157:'157',158:'158',159:'159',160:'160',161:'161',162:'162',163:'163',164:'164',165:'165',166:'166',167:'167',168:'168',169:'169',170:'170',171:'171',172:'172',173:'173',174:'174',175:'175',176:'176',177:'177',178:'178',179:'179',180:'180',181:'181',182:'182',183:'183',184:'184',185:'185',186:'186',187:'187',188:'188',189:'189',190:'190',191:'191',192:'192',193:'193',194:'194',195:'195',196:'196',197:'197',198:'198',199:'199',200:'200',201:'201',202:'202',203:'203',204:'204',205:'205',206:'206',207:'207',208:'208',209:'209',210:'210',211:'211',212:'212',213:'213',214:'214',215:'215',216:'216',217:'217',218:'218',219:'219',220:'220',221:'221',222:'222',223:'223',224:'224',225:'225',226:'226',227:'227',228:'228',229:'229',230:'230',231:'231',232:'232',233:'233',234:'234',235:'235',236:'236',237:'237',238:'238',239:'239',240:'240',241:'241',242:'242',243:'243',244:'244',245:'245',246:'246',247:'247',248:'248',249:'249',250:'250',251:'251',252:'252',253:'253',254:'254',255:'255'};

declare function getValue<T extends { [index: number]: any }, N extends number>(obj: T, idx: N): T[NumberToString[N]];

const value = getValue(['a', 'b', 'c'], 0); //=> any

@KiaraGrouwstra
Copy link
Member

Thanks for the changes.

As to type-level issues, I'm still trying to figure that out, until like yesterday accidentally used older global tsc for typical tests; still got some issues left. Your work here looks pretty much ready though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants