twop / ts-union

ADT sum type in typescript
MIT License
70 stars 2 forks source link

How do I create a generic union of all variants? #4

Open Kinrany opened 5 years ago

Kinrany commented 5 years ago

How do I create an equivalent of this Rust type, including an interface for all of it's members?

enum LoadingEnum<T> {
  Loading,
  Loaded(T)
}
const LoadingEnum = Union(T => ({
  Loading: of(null),
  Loaded: of(T)
}));
type LoadingEnum<T> = ???
twop commented 5 years ago

Hey, thanks for the question!

does this snippet work?

import { Union, of, GenericValType } from 'ts-union'

const LoadingEnum = Union(T => ({
  Loading: of(null),
  Loaded: of(T)
}));

type LoadingEnum<T> = GenericValType<T, typeof LoadingEnum.T>;
Kinrany commented 5 years ago

It does, thanks!

Kinrany commented 5 years ago

Next question: how do I get the type of a payload of a variant of a generic union?

// `Cow` is `{cow: 'cow'}`
type Cow = ???<LoadingEnum<{cow: 'cow'}>, 'Loaded'>;
twop commented 5 years ago

Maybe im missing something but why not:

type Cow = {cow: 'cow'};

basically if you have a snippet like:

const Maybe = Union( a=> ({
 Just: of(a),
 Nothing: of(null)
}))

type Maybe<T> = GenericValType<T, typeof Maybe.T>

const printIfExists = <T>(maybe: Maybe<T> ) => {...}

You essentially want to extract T type from it. Is there a situation when you don't know T?

If so could you give a little bit more context around your question?

Kinrany commented 5 years ago

Yeah, I don't know T. More accurately, it's defined in another module, and I don't want to depend on it. I was trying to use the new enum type to rewrite an equivalent of MaybeLoaded['Loaded'].

// maybe.ts
export const Maybe<T> = ...
export type Maybe<T> = ...

// thing.ts
export type Thing = ...

// maybeThing.ts
import {Maybe} from './maybe';
import {Thing} from './thing';
export type MaybeThing = Maybe<Thing>;

// doStuff.ts
import {MaybeThing} from './maybeThing';
export function doStuff(maybeThings: MaybeThing[]) {
  const things: PayloadOf<MaybeThing, 'Just'>[] = /* filter actual things */
}

It does look like a weird problem to have, but unfortunately it's not something I can easily change.

twop commented 5 years ago

Ok. In that particular case you want to extract back the type of the generic union given the existing one. Let me try to brainstorm:

type ExtractThing<T> = T extends Maybe<infer Thing>? Thing: never;

type Payload = ExtractThing<MaybeThing> ;

I think it will work

Kinrany commented 5 years ago

Oh, right. This should work, thanks again :D

Kinrany commented 5 years ago

Wait, what if it's not a generic type, or a generic type that uses the type argument in a different way?

twop commented 5 years ago

Could you give me examples? :)

Kinrany commented 5 years ago
const MaybePair = Union(T => ({
  None: of(null),
  Some: of<typeof T, typeof T>()
})
type MaybePair<T> = ...

type MaybePairOfThings = MaybePair<Thing>;

type PairOfThings = /* an equivalent of MaybePairOfThings['Some'] */
twop commented 5 years ago

That is currently not supported :( It is definitely doable but would require a lot of typescript magic.

Kinrany commented 5 years ago

Oh well.

A tangentially related idea: what if there was a Variant<TTag, TValue> type, and the Union function merely returned a dictionary of functions that returned Variants? Like a structural analogue to the nominal sum types in other languages.

It'd be possible to solve the problem above with just T extends Variant<'Some', infer TValue> ? TValue : never.

Kinrany commented 5 years ago

Full disclosure: I've already tried to implement this, but types are hard. Now I'm hoping that someone will solve it before I have to try again out of frustration :P

twop commented 5 years ago

If you have a sketch of this that is backward compatible with the current api im interested in exploring that

twop commented 5 years ago

Also curios do you have an actual usecase for having a generic pair?

Kinrany commented 5 years ago

Also curios do you have an actual usecase for having a generic pair?

I'm not entirely sure it's a good case, but I was trying to reimplement a function withLoading that converts an arbitrary React-like render function (args: T) => Node into an equivalent of (args: {loading: undefined} | {loaded: T}) => Node.