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

Can the documentation be made a little less opaque? #3

Open
matthew-dean opened this issue Jan 16, 2024 · 5 comments
Open

Can the documentation be made a little less opaque? #3

matthew-dean opened this issue Jan 16, 2024 · 5 comments

Comments

@matthew-dean
Copy link

I think this library is one that I'm looking for, but I'm finding it extremely difficult to reason about the core types and the problems they solve. Mostly what's missing are examples, like real-world data where the resulting type is narrowed (or expanded? 🤔 ) by using a free type in a way that TS's utility types don't do.

For example, in the documentation, you jump straight into Type, and it says we're typing arguments. Arguments of what? A function? A function defined where? How does this type affect the input and output types of a given function? How do functions that have a type with just Input in Type differ from those with Output?

I feel like this is somewhat like allowing generics to infer generics, but I can't really tell. I know it's difficult to write documentation be putting aside that you already know exactly what's happening, but even with an advanced knowledge of TypeScript types, I feel like there isn't a lot of bridging to what this library is for and how it's used.

@matthew-dean
Copy link
Author

matthew-dean commented Jan 16, 2024

I guess what I'm not seeing is how the constraint itself can be an inferred generic (which receives arguments)? 🤔

In other words, say I have this interface:

interface One<T extends Record<string, any>> {
  data: T
}

And then I make a new interface like:

interface NewOne<T extends Record<string, any>> extends One<T> {
  data2: T
}

Then I have a function like:

const fn = <T extends One[]>(plugins: T, data: Record<string, any>) => ...

This is an oversimplification, but I'd like the resulting output to be strongly typed based on data passed into the function, in other words, I want to iterate through T in the function, and re-apply either One or NewOne to create a const array with strongly-typed data and data2 properties. (I want to iterate and "call" NewOne<typeof data> and One<typeof data> based on which interface is being extended in plugins.

@geoffreytools
Copy link
Owner

geoffreytools commented Jan 17, 2024

Hi matthew-dean,

Thanks a lot for your feedback. Despite my best efforts I was indeed a bit expeditious in some places and I am relatively blind to what needs to be explained.

A word of warning: free-types do not allow you to infer things which are otherwise not inferable. They simply give you a way to articulate "type constructor" or "type parameter" separately when you write a type (to clarify: by providing TS identifiers which target the constructor and the argument separately, they can indeed help infer things, but you won't overcome compiler limitations since free-types are written in TS).

When I talk about "arguments" and "parameters" I mean the arguments and parameters of the free type constructor. They are distinct from type parameters the same way type parameters are distinct from function parameters, it's another level of abstraction, but it is the same metaphor.

You can find an example of how to connect the value level and the type level in the Dependency Inversion section of the Use Cases. which gives an example of how you can infer a free type constructor from a function in a higher order function.

I am completely rewriting the documentation for the next major version (and including a healthy dose of JSDoc).

--
Sorry I must have hit a key combination that closed the issue and submitted the comment while I was typing

@geoffreytools
Copy link
Owner

Regarding your use case, I am not certain free-types are necessary. Could you be more precise or link to a stackoverflow question?

@matthew-dean
Copy link
Author

matthew-dean commented Jan 17, 2024

Hmm @geoffreytools - I found this library from a post you made for this issue, in which there's a proposal to be able to pass generics into types, so that you can pass type parameters into those generics.

You wrote:

I made some progress on this issue on the user end of things : free-types

So, I inferred that you had found a way to solve this issue (at least partially), that is, the issue of being able to pass generics into types and thus dynamically pass type args into those generics.

Are you saying that isn't the case?

@geoffreytools
Copy link
Owner

geoffreytools commented Jan 17, 2024

People have weird expectations regarding HKT and it is not clear what you need to do, so I am testing the water ;)

You can find bellow an example derived from your code where unwrap is used to decide between 2 type constructors and apply is used to wrap the new value with the "inferred" one.

import { Type, A, apply, unwrap } from 'free-types';

/** add `data` to elements of the plugins array */
const merge = <const T extends readonly Plugin[], const D extends Data>(plugins: T, data: D) => 
    plugins.map(plugin => ({
        data: { ...plugin.data, ...data },
        ...('data2' in plugin ? { data2: { ...plugin.data2, ...data } } : {})
    })) as Merge<T, D>;

/* the Merge utility type */

type Merge<T extends readonly Plugin[], D extends Data> = {
  [K in keyof T]: apply<unwrap<T[K], [$One, $NewOne]>['type'], [Override<T[K]['data'], D>]>;
}

type Override<A, B> = unknown & {
  [K in keyof A | keyof B]: 
    K extends keyof A & keyof B
    ? A[K] | B[K]
    : K extends keyof B
    ? B[K]
    : K extends keyof A
    ? A[K]
    : never;
};

/* your types */

type Data = Record<string, any>;

type Plugin = One<Data> | NewOne<Data>;

interface One<T extends Data> {
  data: T
}

interface NewOne<T extends Data> extends One<T> {
  data2: T
}

/* value constructors */

const one = <const D extends Data>(data: D): One<D> => ({ data });
const newOne = <const D extends Data>(data: D): NewOne<D> => ({ data, data2: data });

/* free type constructors */

interface $One extends Type<[Data]> { type: One<A<this>> }

interface $NewOne extends Type<[Data]> { type: NewOne<A<this>> }

/* execution */

const r = merge([one({ a: 1 }), newOne({ b: 2 }), one({ c: 3 })], { d: 4 });
//    ^? const r: readonly [One<{ a: 1; d: 4; }>, NewOne<{ b: 2; d: 4; }>, One<{ c: 3; d: 4; }>]

console.log(r);
/*
  [{
    data: { a: 1, d: 4 }
  }, {
    data: { b: 2, d: 4 },
    data2: { b: 2, d: 4 }
  }, {
    data: { c: 3, d: 4 }
  }]
*/

playground

Now do you really need higher kinded types for this (as in: it would not be possible without free-types)? No, you could use conditional types to match elements of your tuple against One<Data> and NewOne<Data> (in fact that's what unwrap is doing).

This is one of these instances where I can say free-types don't allow you to infer things which are not otherwise inferable: I had to provide unwrap with a list of free type constructors to match the input against (I could also have registered them in TypesMap, which is a good option if you want to make Merge reusable, provided the type constructors you register are not mutually structurally compatible).

I hope this answer helps. I haven't touched TS in months so my fingers are a little numb. Please keep adding points which you find frustrating or unclear about the way the lib is presented, it is very enlightening.

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