Skip to content
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

Impossible to Describe generic types with conditional #1163

Open
dsluijk opened this issue Dec 28, 2022 · 2 comments
Open

Impossible to Describe generic types with conditional #1163

dsluijk opened this issue Dec 28, 2022 · 2 comments

Comments

@dsluijk
Copy link

dsluijk commented Dec 28, 2022

I cannot figure out how to describe generics with conditionals. This might be a limitation by TypeScript though.

Given the following types:

enum Types {
  One = "one",
  Two = "two",
}

type Validating<T extends Types = Types> = {
  type: T;
  value: T extends Types.One ? string : never;
};

I can't figure out how to type value in Validating. This is my general idea:

const Validating: Describe<Validating> = union([
  object({
    type: literal(Types.One),
    value: string(),
  }),
  object({
    type: literal(Types.Two),
  }),
]);

assert({ type: Types.One, value: "Value!" }, Validating);
assert({ type: Types.Two }, Validating);

But this does not compile, giving the following error:

Type 'Struct<{ type: Types.One; value: string; } | { type: Types.Two; }, null>' is not assignable to type 'Describe<Validating<Types>>'.
  Types of property 'TYPE' are incompatible.
    Type '{ type: Types.One; value: string; } | { type: Types.Two; }' is not assignable to type 'Validating<Types>'.
      Type '{ type: Types.Two; }' is not assignable to type 'Validating<Types>'.

I also had a second idea which is even more broken, but is nicer if possible.

const Validating: Describe<Validating> = dynamic((v: any) =>
  object({
    type: literal(Types.One),
    value: v.type === Types.One ? string() : never(),
  }),
);

assert({ type: Types.One, value: "Value!" }, Validating);
assert({ type: Types.Two }, Validating);

I think this is related to #724, but not quite the same.

@dsluijk
Copy link
Author

dsluijk commented Dec 29, 2022

I think I narrowed down the main (two) issue(s).

type Validating<T extends Types = Types> = {
  type: T;
  value: T extends Types.One ? string : never;
};

const Validating: Describe<Validating> = object({
  type: enums(Object.values(Types)),
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  value: dynamic((_, ctx): any => {
    const type: Types = ctx.branch.at(-2)?.type ?? Types.One;
    if (type === Types.One) {
      return string();
    } else {
      return literal(undefined);
    }
  }),
});

This works, but requires you to set any on the return type of the dynamic.

The second issue is when you make the value an array of strings, not a string.

type Validating<T extends Types = Types> = {
  type: T;
  value: T extends Types.One ? string[] : never;
};

const Validating: Describe<Validating> = object({
  type: enums(Object.values(Types)),
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  value: dynamic((_, ctx): any => {
    const type: Types = ctx.branch.at(-2)?.type ?? Types.One;
    if (type === Types.One) {
      return array(string());
    } else {
      return literal(undefined);
    }
  }),
});

This Gives you a cryptic type error, but this can be fixed by manually overriding the value type in the object like so:

const Validating: Describe<Validating> = object({
  type: enums(Object.values(Types)),
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  value: dynamic((_, ctx): any => {
    const type: Types = ctx.branch.at(-2)?.type ?? Types.One;
    if (type === Types.One) {
      return array(string());
    } else {
      return literal(undefined);
    }
  }) as unknown as Describe<Validating["value"]>,
});

Still don't really have a nice fix for this, but hopefully this helps someone else facing the same issue.

@morlay
Copy link

morlay commented Apr 24, 2023

Could use Discriminated Union instead

export function discriminatorMapping<
  D extends string,
  Mapping extends Record<string, TypeAny>
>(
  discriminator: D,
  mapping: Mapping
): Struct<Simplify<DiscriminatedUnion<D, Mapping>>, null> {
  return  dynamic<any>((v: any = {}) => {
    const discriminatorValue = (v as any)[discriminator];
    const matched = mapping[discriminatorValue];

    if (typeof (v as any)[discriminator] === "undefined" || !matched) {
      return object({
        [discriminator]: enums(Object.keys(mapping))
      });
    }

    return object({
      [discriminator]: literal(discriminatorValue),
      ...matched.schema
    });
  });
}


type DiscriminatedUnion<
  D extends string,
  Mapping extends Record<string, TypeAny>
> = ValueOf<{
  [K in keyof Mapping]: { [k in D]: K } & Infer<Mapping[K]>;
}>;

type ValueOf<T> = T[keyof T];
enum NetType {
  AIRGAP = "AIRGAP",
  DIRECT = "DIRECT",
}

const t: Struct<{ netType: NetType.AIRGAP } | { netType: NetType.DIRECT, endpoint: string  }> = discriminatorMapping("netType", {
    [NetType.AIRGAP]: object(),
    [NetType.DIRECT]: object({
      endpoint: string()
    })
  }
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants