-
Notifications
You must be signed in to change notification settings - Fork 13
Provide a function to generate data constructors? #2
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
Comments
Yes, I've thought about this quite a bit, and unfortunately, haven't come up with something I'm really happy with. I'd really like to come up with a solution for this, though, since this is the thing I most miss from other languages! I typically do exactly what you said and build "smart constructors" for each type: export type These<L, R> = ADT<{
left: { left: L },
right: { right: R },
both: { left: L, right: R }
}>
export const left = <L>(left: L): These<L, never> => ({_type: 'left', left})
export const right = <R>(right: R): These<never, R> => ({_type: 'right', right})
export const both = <L, R>(left: L, right: R): These<L, R> => ({_type: 'both', left, right}) These allow me to fine-tune the parameters and return types to my liking (fixing the type on the super type for better inference, supplying never, etc) A brain dump of my thoughts:
|
@pfgray that makes sense! Ya, I was wondering exactly how this could be done in TS. The idea of fine-tuning is a great point too - it might end up that generated constructors would be too generic in the end anyway. Feel free to close this, I appreciate the response! As I learn more about Typescript if I come up with anything I'll be sure to share it. |
@pfgray first of all thank you for this package! I miss proper adts in ts. Regarding the generics issue in the io-ts approach: I think they have an example where they show how they do it https://github.com/gcanti/io-ts/blob/master/README.md#generic-types |
I was just about to open an issue about this topic. And see, there's only one open and it's exactly about this. So, here's my take on the problem: Basically we want to reduce some boilerplate for a common problem. As already stated there is no way to go from a type to a value in TypeScript. The other way around is possible so it's worth investigating. However, this has also been mentioned, it's getting problematic with generic type arguments. const foo = <T>() => ({
_tag: "cup",
of: {} as T,
}); How do we get the type Here's a try: type Foo = ReturnType<typeof foo> resolves to:
So I think there is no way around a certain amount of repetition. An I think that's kind of ok, to consider the type as the source of truth and some values that have to match with it. It has also been noted that any generated constructors may bee to generic in the end. Sometimes you want to have positional arguments, sometimes a record, sometimes a mix, and sometimes you even want to have some setup code inside the constructor. Ok, let's manually write some constructors that have those custom properties: I find it useful to define the ADT in two steps. By this, you can reference the non-union definitions in your constructors. type FatalError<Id> = { atFile: string; id: Id };
type HorribleError = { count: number };
type Error<Id> = ADT<{
fatalError: FatalError<Id>;
horribleError: HorribleError;
}>; And here are the hand crafted constructors: const error = <Id>() => ({
// position example
fatalError: (
atFile: FatalError<Id>["atFile"],
id: FatalError<Id>["id"]
): Error<Id> => ({
_type: "fatalError",
atFile,
id,
}),
// record exmaple
horribleError: (opts: HorribleError): Error<Id> => ({
_type: "horribleError",
...opts,
}),
}); How can we reduce a little boilerplate here? That's how I would like to write it: onst error_ = <Id>() =>
makeConstructors<Error<Id>>({
// position example
fatalError: (
atFile: FatalError<Id>["atFile"],
id: FatalError<Id>["id"]
) => ({
atFile,
id,
}),
// record exmaple
horribleError: (opts: HorribleError) => opts,
}); Note that the trivial case ( And the implementation of const makeConstructors = <Cases extends Record<string, {}>>() => <
Ctors extends Record<keyof Cases, (...args: any[]) => any>
>(
ctors: Ctors
): { [key in keyof Ctors]: (...args: Parameters<Ctors[key]>) => ADT<Cases> } =>
pipe(
ctors,
record.mapWithIndex((_type, f: any) =>
flow(f, (rec: any) => ({ ...rec, _type } as ADT<Cases>))
)
) as any; It basically just composes the functions with other ones that add the correct tag to the result. And it attaches the correct union type as the return type, which is nice for error messages. the derived type of the constructors is then:
And it seems to work: console.log(error().horribleError({ count: 2 }));
// { count: 2, _type: 'horribleError' } What do you think? Would this be a viable way to simplify constructor creation? |
I think this is a great idea! Here's my attempt. It's not ideal - a value-level keys must be passed in (unless we used something like ts-transformer-keys). It's not pretty, but it works Spec: import { ADT, constructors } from 'ts-adt'
type Xor<A, B> = ADT<{
nothing: {};
left: { value: A };
right: { value: B };
}>;
const xorCtors = <A, B>() => constructors<Xor<A, B>>()(['nothing', 'left', 'right'])
const { nothing, left, right } = xorCtors<number, string>()
const n: Xor<never, never> = nothing()
const l: Xor<number, never> = left({ value: 3 })
const r: Xor<never, string> = right({ value: 'text' }) Implementation: import * as S from 'fp-ts/Semigroup'
import * as R from 'fp-ts/Record'
import * as A from 'fp-ts/ReadonlyArray'
const makeConstructors = <TagName extends string>(
tagName: TagName
) => <ADT extends { [tag in TagName]: string }>() => <Tags extends ADT[TagName]>(
tags: readonly Tags[]
) => R.fromFoldableMap(
S.last<unknown>(),
A.Foldable
)
(
tags,
(tag) => [
tag,
(args: undefined | Omit<Extract<ADT, { [t in TagName]: Tags }>, TagName>) =>
args ? { [tagName]: tag, ...args } : { [tagName]: tag }
]
) as {
[key in Tags]:
keyof Omit<Extract<ADT, { [t in TagName]: key }>, TagName> extends never
? () => Extract<ADT, { [t in TagName]: key }>
: (args: Omit<Extract<ADT, { [t in TagName]: key }>, TagName>) =>
Extract<ADT, { [t in TagName]: key }>
}
const constructors = makeConstructors('_type') I'd be willing to make a PR for this w/ tests if anyone's interested |
There's a proposal to Typescript that would allow this. It's an interesting read. Hopefully the feature is added soon. Fwiw I agree that this would be the best solution. Afaict this would turn It looks like it might not be possible for a little while, but I understand if there's more energy behind holding out for an ideal solution than a compromise |
practical-fp/union-types has a clever solution to this problem using a Proxy object |
Hi @pfgray - I just stumbled upon this lib and it is really neat! I was wondering if you have a way to hide the
_type
property from calling code? Going with theOption
example from the readme if I wanted to create asome
value I can writebut it seems like it might be useful to have a way to write
Maybe the library could provide a function that generates these for you? Curious if you have any thoughts on this? Thanks again for sharing this code!
The text was updated successfully, but these errors were encountered: