Giter VIP home page Giter VIP logo

Comments (6)

geoffreytools avatar geoffreytools commented on June 16, 2024 1

Hi,

No problem, I consider it part of the documentation.

People often mention higher kinded types when this use case comes up, but no implementation of HKT in TS can apply a generic function type with an argument dynamically, because the parameters in a generic function are not of the same kind as the parameters in a generic type alias or interface and TS doesn't provide any hook to access them at the type level.

However type-lenses may help if you are ready to take a risk.

First, let me suggest something simpler that we can do at the value level, because it may be enough for your use case.

declare function validate <Request>(req: Request): { validated: Request };

// we can actually do this at the value level
const validateFooString = validate<Paths['/route']['get']['request']>

const api: Api<Paths, typeof validateFooString> = {
  before: validate,
  api: {
    "/route": {
      get: (req, before) => {
        before.validated
         //        ^? (property) validated: { foo: string; }

        return (before as any).validated.foo
      }
    }
  }
}

playground

Now if we want to do this dynamically, an idea is to replace unknown in ReturnType<B> with P[path][method]['request'], which is not what we want semantically but yields the same result in this instance.

type-lenses require a path to replace some piece of type with another. We can write a utility type that looks for unknown in an arbitrarily nested object and generates the path to go there.

// create a path leading to `unknown` in the object type
type CreatePath<T> = FilterPath<GetPaths<T>>

// get all possible paths and the value they lead to in the object type
type GetPaths<T> = T extends object
    ? { [K in keyof T]: [K, ...GetPaths<T[K]>] }[keyof T]
    : [T];

// exclude paths which don't lead to `unknown`
type FilterPath<T> = T extends [unknown, ...unknown[]]
  ? IsUnknown<Last<T>> extends true ? Init<T> : never
  : never;

With this path it is now possible to leverage Replace

// in API
before: Replace<CreatePath<ReturnType<B>>, ReturnType<B>, P[path][method]['request']>

No change needs to be made to api

const api: Api<Paths, typeof validate> = {
  before: validate,
  api: {
    "/route": {
      get: (req, before) => {
        before.validated
        //        ^? (property) validated: { foo: string; }

        return (before as any).validated.foo
      }
    }
  }
}

playground

Now the risk I mentioned is that unknown is a perfectly valid type which could appear multiple times in an object, and not necessarily in places you want to find/replace, so use this with caution!

from free-types.

ChuckJonas avatar ChuckJonas commented on June 16, 2024

First off, seriously thank you so much for taking the time to write out such a lengthly and complete response! 🙇

let me suggest something simpler that we can do at the value level, because it may be enough for your use case.

Unfortunately this isn't sufficient for my use case. In my attempt to make the example code as simple as possible, I removed some critical context. The goal is to get the before response type to work across various routes, based on the request types of each route:

Playground

It's going to take me a bit to fully grok this, but it definitely seems inline with what I'm looking for!

I do have one immediate followup question....

Why, if I replace the return type of validated with something that doesn't use the generic parameter Response, does the type before becomes the same as request?

I assume it has something to do with the conditional types used here Replace<CreatePath<ReturnType<B>>, ReturnType<B>, P[path][method]['request']> somehow looking for the type of P[path][method]['request'] and falling back to P[path][method]['request'] when it's not found?

Playground

from free-types.

geoffreytools avatar geoffreytools commented on June 16, 2024

You're welcome.

The issue here is that CreatePath<{ validated: boolean }> returns never, because there is no path leading to unknown. The behaviour of Replace is unspecified when no path is provided.

I would compare CreatePath<ReturnType<B>> with never and branch over that

before: CreatePath<ReturnType<B>> extends infer Path extends Query
  ? [Path] extends [never]
    ? ReturnType<B>
    : Replace<Path, ReturnType<B>, P[path][method]['request']>
  : never

Note that you need to import the constraint Query from type-lenses.

playground

This reminds me that when using ReturnType on a generic function, if the generic has a type constraint, it's going to be substituted with the type constraint instead of unknown, and as a result CreatePath won't detect any path either. One way to deal with this could be to look for supertypes of P[path][method]['request']> instead of unknown when generating the path, but as you can imagine the risk of collision with legitimate types becomes very high.

from free-types.

ChuckJonas avatar ChuckJonas commented on June 16, 2024

Awesome thanks!

I've been messing around with type-lenses for the last hour and starting to get it. Still TBD if I'll have the skills to incorporate this back into the full vision, but such a cool little utility library!

I think I enjoy writing types more than the code itself 😅

from free-types.

geoffreytools avatar geoffreytools commented on June 16, 2024

Yeah that's something to be wary of actually ;)

By the way, this issue made me consider including path finding to type-lenses, and changing the behaviour of Replace when the path is never so that users don't need to branch.

Don't hesitate to post issues if the documentation wasn't very clear. I don't get a lot of feedback.

from free-types.

geoffreytools avatar geoffreytools commented on June 16, 2024

type-lenses now implements FindReplace for this kind of use case, so this:

before: CreatePath<ReturnType<B>> extends infer Path extends Query
  ? [Path] extends [never]
    ? ReturnType<B>
    : Replace<Path, ReturnType<B>, P[path][method]['request']>
  : never

can be re-written as:

before: FindReplace<ReturnType<B>, unknown, [P[path][method]['request']]>

Notice the brackets around P[path][method]['request']. FindReplace expects a tuple of replace values (or a replace callback)

Replace now fails gracefully when the query is never, and a general FindPath utility type was added to the library, so the following would also work:

before: Replace<FindPath<ReturnType<B>, unknown>, ReturnType<B>, P[path][method]['request'], any>

Notice the any at the end. Replace now type checks the replace value against the query, which is problematic when it requires a generic to be resolved (here B). any lets us disable the check.

from free-types.

Related Issues (3)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.