0

I've got a setup like so. The idea is that I want to accept a generic and an options shape, infer the options necessary from the generic and the type key of the options shape, and constrain the options appropriately.

type OptionProp<T extends string | boolean> = {
  defaultValue: T;
} & (T extends string
  ? { type: "select"; options: T[] } | { type: "text" }
  : { type: "checkbox" });

function useFixtureOption<T extends string>(
  name: string,
  prop: OptionProp<T>
): T {
  console.log(prop);
  return prop.defaultValue;
}

useFixtureOption<"foo" | "bar">("field", {
  type: "select",
  options: ["foo", "bar"],
  defaultValue: "foo",
});

However, Typescript is confused about my options type, defined as T[], and thinks that it's "foo"[] | "bar"[] rather than ("foo" | "bar")[]:

enter image description here

If I don't use a conditional type here, everything is fine:

type OptionProp<T extends string> = {
defaultValue: T;
} & ({ type: "select"; options: T[] } | { type: "text" });

But I want to be able to enforce that a boolean T requires type: checkbox and that text/select require a string T.

I feel like there's some subtlety of the type system that I'm missing here, but I have no clue where it is. My best guess is that the T extends string check is over-narrowing the type and producing a type option per member of the T union. Is that correct, and if so, how do I deal with it?

Playground link included!

Edit: I've apparently discovered distributed conditional types. F<A|B> = F<A> | F<B>, which makes sense as to why I'm seeing what I'm seeing now. Now, how to fix it?

1 Answer 1

2

I've tracked this down. I've run into a feature called distributive conditional types which takes union and distributes it, resulting in a new type F<A|B> = F<A> | F<B>.

The fix is straightforward and from the manual:

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

type OptionProp<T = any> = ([T] extends [string]
  ? { type: "text" } | { type: "select"; options: T[] }
  : { type: "checkbox" }) & {
  defaultValue: T;
  value?: T;
};
Sign up to request clarification or add additional context in comments.

2 Comments

Upvoted! Btw, no need to use explicit generic parameter in useFixtureOption<"foo" | "bar">
Not in this example, but it was included for clarity and because in my actual use case I want the options to be enforced by the type to make sure the IDE helps catch invalid options! :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.