dillonkearns / elm-typescript-interop

Generate TypeScript declaration files for your elm ports!
BSD 3-Clause "New" or "Revised" License
165 stars 13 forks source link

Write out TypeScript Interfaces for type aliases #6

Open muelli opened 6 years ago

muelli commented 6 years ago

I have quite a few ports and I think I would enjoy if elm-typescript-interop would write out my Elm type aliases along with the actual signature of the port.

Let me explain a little.

If have a model Player (adapted for brevity):

type alias Player =
    { pk : String
    , lastname : String
    , firstname : String
    }

and I have many ports sending or receiving that player. Now with three elements in the record, the generated d.ts files are still comprehensible. But if the Player model grows, the generated file contains a lot of repetitive elements.

elm-typescript-interop could generate something like

export interface IPlayer {
    pk: string;
    lastname: string;
    firstname: string;
}

and use that instead in the function definitions.

Then the question is when to actually do that. One idea is to have annotations, e.g. in comments:

-- @TSIO: interface: PlayerI
type alias Player =
    { pk : String
    , lastname : String
    , firstname : String
    }
dillonkearns commented 6 years ago

Now with three elements in the record, the generated d.ts files are still comprehensible. But if the Player model grows, the generated file contains a lot of repetitive elements.

Hello @muelli, could you help me understand what problem this addresses? I'm trying to understand the value here to help me prioritize things better. Especially because this is a non-trivial task, I'd like to get some sense of what use cases this helps with.

You mention making the .d.ts files more comprehensible. But that doesn't seem necessary to me, it's just an internal detail that's hidden from the user, right? Is there anything else that this would help with. I would love to know if there is a specific problem that you've encountered yourself that would be improved by a feature like this.

I appreciate you starting the discussion, thanks!

sheakelly commented 6 years ago

We have just adopted this project and it has been really helpful so far. Thanks for your awesome work @dillonkearns

On our project it would be helpful to have the port arguments generated as exported types so that we can use them in the port handling functions in typescript. Right now we are are defining our own types that matches the type for the port argument.

muelli commented 6 years ago

You mention making the .d.ts files more comprehensible. But that doesn't seem necessary to me, it's just an internal detail that's hidden from the user, right? Yes and no.

For me, I want to continue working with these types in other functions. Currently, a change in the Elm type rightfully causes a change in the .d.ts file. But it also causes a change in all the other function signatures that consume that type. That change I have to perform manually. It seems avoidable to me having to touch all my functions taking a Player if all I do is adding a field to the type. If there was an Interface, I could simply consume that in my functions. In fact, that's what I'm doing manually now and I have to watch out for the .d.ts and my Interface not going out of sync too much.

And because I have to ship the .d.ts file (because parsing my Elm codebase fails, so I cannot generate the file on the fly. Even if I could, I wouldn't know how to actually make it happen with the Webpack setup I have) I tend to review changes in it every so often. And with big types, it's hard to see changes in the type definitions. But I appreciate that this is a fringe case that shouldn't exist.

caseyWebb commented 4 years ago

I've came up with a fairly decent utility type to use as a workaround

type ExtractElmPortData<
  TProp extends keyof Elm.Main.App['ports']
> = Elm.Main.App['ports'][TProp] extends {
  subscribe(callback: (data: infer TData) => void): void
}
  ? TData
  : Elm.Main.App['ports'][TProp] extends {
      send(data: infer TData): void
    }
  ? TData
  : unknown

type RecipeOut = ExtractElmPortData<'receiveRecipe'>
type RecipeIn = ExtractElmPortData<'saveRecipe'>

and the .d.ts generated by elm-typescript-interop...

export namespace Elm {
  namespace Main {
    export interface App {
      ports: {
        receiveRecipe: {
          send(data: { slug: string; name: string; ingredients: { name: string }[] }): void
        }
        saveRecipe: {
          subscribe(callback: (data: { slug: string; name: string; ingredients: { name: string }[] }) => void): void
        }
      };
    }
    // ...
  }
}
muelli commented 4 years ago

Cool. This makes types for the argument to subscribe and send, right? It is possible to further split the types up? I.e. to have a type for name or ingredient?

muelli commented 4 years ago

The answer is: yes.

type Ingredient = RecipeIn['ingredients'][0]

This is very amazing. Thanks for pointing these types out.