Proposal: Should be able to pull out the expected input type from a build domain function
jjhiggz opened this issue · 3 comments
Aloha!
I was going to try and use domain functions to build out a type safe fetcher example. One of the things that I think could be really useful for this would be if I could pull out the "Expected Input" type from a built domain function, so that way we would know what to plug into the fetcher and get across the wire typesafety.
Basically the goal would be that you could do something like this
const addCharacter = makeDomainFunction(z.object({name: z.string()}))(
async (newCharacter) => {
return await characters.push({
...newCharacter,
id: randomInteger(1, 1000000),
});
}
);
type InferredInput = InferDomainFnInput<typeof addCharacter> // { name: string }
I actually was able to accomplish this pretty easy with just a couple of lines of code by modifying these definitions.
declare type DomainFunction<Output = unknown, InputSchemaType = undefined> = {
(input: unknown, environment?: unknown, expectedInput?: InputSchemaType): Promise<Result<Output>>;
// added "expectedInput" which will never actually be used as an argument but can be positionally used to track the expected
};
And also modified the MakeDomainFunction
type
declare type MakeDomainFunction = <Schema extends z.ZodTypeAny, EnvSchema extends z.ZodTypeAny>(inputSchema: Schema, environmentSchema?: EnvSchema) => <Output>(handler: (inputSchema: z.infer<Schema>, environmentSchema: z.infer<EnvSchema>) => Promise<Output>) => DomainFunction<Output, z.infer<Schema>>;
// Plugged in the expected input as the second generic for `DomainFunction`
Now you can pull out the expected input by doing
type InferredDomainFnInput<T> = Exclude<Paramaters<typeof addCharacter>[2], undefined>
^^^ This is the end of the mechanics on how to actually extract the input value, which I imagine would have some valid use cases outside of what I'm about to describe. But keep reading if you want to see the grander vision
Now in theory you could build out fetchers in remix by wrapping useFetcher
with a custom hook that you can plug the typeof a domain-fn into. So basically you could structure nearly all of your actions like this.
import { myDomainFn } from "./domain-fns"
const action = ({req}) => {
return myDomainFn(await req.body)
}
and you could generate a routes.types.ts that looks something like this
import { domainFn1 } from "./domain-fns/df1.ts"
import { domainFn2 } from "./domain-fns/df2.ts"
export type Routes = {
"path-to-route-1": InferDomainFnInput<typeof domainFn1>,
"path-to-route-2": InferDomainFnInput<typeof domainFn2>
}
Or something like that ^ then build a useFetcher
hook that allows you to select a path from the key of those routes
In theory it would be better typesafety if you could plug into the action itsself which domain-fns are being used. But I haven't figured out how to do that.
Hi @jjhiggz thanks for taking the time for this proposal. The expected input type of a DF get very tricky very fast, that's why so far we haven't exposed it. You can see a bit of this history here.
The composition is where things start to get dificult since you have to calculate new types based on multiple DFs. Another problem with this sort of approach is that would only capture the expected type inside the DF. You would have no knowlege of how the parser works. Just to illustrate, imagine a DF:
const cannotBeCalled = mdf(
z.preprocess(() => {
throw new Error('You cannot know what the parser does from its type')
}, z.number()),
)((input) => input)
It might strike you as a silly example, but the principle applies to several cases. It's just in the way domain functions are designed (to be insulated from an unstructured world using parsers).
However, if you take the parsers out, one could do something like that, and compose the parsers just before plugging the function in your action. I believe this will be soon possible using a new type called Composable
. You can see some of that work in progress.
I'm closing this issue but feel free to open a discussion in case you have another proposal in mind or just want to further discuss this topic.
@diogob Thank you for explaining that! Seems like it will be extremely useful when you get it figured out.
Hey @jjhiggz the new version of this library - which is called composable-functions and was released today - should work as you expect!
Now the arguments of a functions are in the type:
const add = composable((a: number, b: number) => a + b)
// ^? Composable<(a: number, b: number) => number>
Compositions are also type-checked and you shouldn't be able to compose functions that have unmatching arguments.
Take a look on the README and let us know how it goes!