3

I've looked at other answers that appear to be asking a simplified version of my question, but I can't figure out how to apply them to my specific question. The following code shows what I'm trying to achieve:

type MyStructTrue = {
  future_key: string;
  future_value: true;
};

type MyStructFalsy = {
  future_key: string;
  future_value?: false;
};

type MyStruct = MyStructTrue | MyStructFalsy;

const myStructArray = [
  { future_key: 'true1', future_value: true },
  { future_key: 'false1', future_value: false },
  { future_key: 'false2', future_value: undefined },
  { future_key: 'false3' },
] as const satisfies readonly MyStruct[];

type MyStructUnion = (typeof myStructArray)[number];

// I want to build a type of:

// type KeyValues = {
//   'true1': 'yes',
//   'false1': number,
//   'false2': number,
//   'false3': number,
// }

type KeyValues = {
  // ^?
  [K in MyStructUnion as MyStructUnion['future_key']]: K extends {
    future_value: true;
  }
    ? 'yes'
    : number;
};

To me it appears that K extends { future_value: true; } ? ... is applied to all possibilities of MyStructUnion instead of individual members. As a result, every value of KeyValues is 'yes' | number. But I want it to apply that conditional to specific union members to get the type I describe in my code's comment.

I think part of the solution is to use a Distributive Conditional Type, but the type signature of MyStructFalsy seems to interfere: future_value may not exist as a property so I am unable to access it in a conditional. I haven't figured out a way to wrap anything in Required<...> to prevent this.

How do I achieve the KeyValues type signature in my comment?

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356

1 Answers1

4

As you mentioned in the description, the condition is applied to all possibilities, not to a single member of a union. To fix we should distribute the union, which is possible when we check K extends K. Alternatively you could use K extends any or K extends unknown.

If this condition is true, the following code will be executed for each member of the union separately:

{
   [P in K["future_key"]]: true extends K["future_value" & keyof K]
      ? "yes"
      : number;
}

Since, "future_value" doesn't exist in all members of the union, the K isn't inferred to have it as a key. By writing "future_value" & keyof K I'm telling Typescript to take the intersection of these two, and if there is no "future_value" in keyof K the intersection will result in never, which will result in K[never], which will result in never. Otherwise, the intersection will be "future_value" itself.

If we now write type Result = KeyValues we will get the following type:

{
    true1: "yes";
} | {
    false1: number;
} | {
    false2: number;
} | {
    false3: number;
}

This type is the expected behavior; however, we need an intersection for your expected result, not a union. We can use the type mentioned in here. New version:

type KeyValues<K extends MyStructUnion = MyStructUnion> = UnionToIntersection<
  K extends K
    ? {
        [P in K["future_key"]]: true extends K["future_value" & keyof K] ? "yes" : number;
      }
    : never
>;

Now type Result = KeyValues will result:

{
    true1: "yes";
} & {
    false1: number;
} & {
    false2: number;
} & {
    false3: number;
}

This is the desired result, however, it doesn't look pretty; that's why we will wrap the whole KeyValues type with the following type that makes the intersections disappear by redefining the object:

type Prettify<T> = T extends infer R ? {[K in keyof R]: R[K]} : never;

Final code:

type KeyValues<K extends MyStructUnion = MyStructUnion> = Prettify<
  UnionToIntersection<
    K extends K
      ? {
          [P in K["future_key"]]: true extends K["future_value" & keyof K]
            ? "yes"
            : number;
        }
      : never
  >
>;
/*
type Result = {
    true1: "yes";
    false1: number;
    false2: number;
    false3: number;
}
*/
type Result = KeyValues;

playground

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
wonderflame
  • 6,759
  • 1
  • 1
  • 17
  • I'm confused by that first paragraph and your first example. I think it might be easier if you used `K` to represent the same concept as my `K`, or gave your generic parameters different names. Regardless, I think you are describing Distributive Conditional Types as a "for loop"? Am I understanding that part correctly? – Daniel Kaplan May 04 '23 at 22:58
  • "If this condition is true the following code will be executed for each union separately. If that condition is true, we will just do the same mapping as you did but changed to the way that I decided to go with, however, it does the same thing as yours." Did you mean for these to both say "true"? Did you mean to say "for each union separately" or "for each union member separately"? – Daniel Kaplan May 04 '23 at 22:59
  • 1
    "I think you are describing Distributive Conditional Types as a "for loop"? Am I understanding that part correctly? " Yes, you are completely right – wonderflame May 04 '23 at 23:01
  • `Did you mean to say "for each union separately" or "for each union member separately"?`. Thanks for correcting, yes I meant for each member of union – wonderflame May 04 '23 at 23:02
  • What is `& keyof K` doing in your first example? Why do you need that? Is it a way to protect from typos? "This type is the expected behavior, however, we need an intersection, not a union." Why? – Daniel Kaplan May 04 '23 at 23:05
  • `This type is the expected behavior, however, we need an intersection, not a union." Why?` Do you mean why we need an intersection? We need it because your expected result is the object with all possibilities not a union of possibility. If you are asking why a union is the expected behavior it's because we distributed the `K` and the we will get a union of possibilities – wonderflame May 04 '23 at 23:12
  • "Do you mean why we need an intersection?" Yeah, that's what I meant, thanks. "We need it because your expected result is the object with all possibilities not a union of possibility." Oh, that makes sense to me. Thank you. re: your previous comment, `By writing "future_value" & keyof K I'm telling ts to take the intersection of these two ... never`. Will it become `K[never]`? What will happen next in the evaluation? – Daniel Kaplan May 04 '23 at 23:20
  • " Will it become `K[never]`? What will happen next in the evaluation?" Yes, it will be `K[never]` which will be evaluated as `never` – wonderflame May 04 '23 at 23:21
  • What happens after it is evaluated to `never`? `true extends never ? ...` returns the false branch, so it becomes `number`? Wouldn't you want `'yes'` instead; you said, "there is "future_value" in keyof K the intersection will result never". – Daniel Kaplan May 04 '23 at 23:31
  • Imagine the `&` as the intersection of two sets. First set `['future_value']` and the other `['future_key']`. Their intersection is empty set `[]` a.k.a `never` in our case. However, if the first set is `['future_value']` and the second is `['future_value', 'future_key']`, the intersection will be just `['future_value']`. Thus, if there is `future_value` in the `keyof K` we will get `"yes"`, if it isn't there we get `number` – wonderflame May 04 '23 at 23:34
  • Right... I think I interpreted "there is "future_value" in keyof K the intersection will result never" to mean "it evaluates to `never` if there **is** a "future_value" key`. It sounds like you are saying the opposite though. – Daniel Kaplan May 04 '23 at 23:43
  • forgot a "no" in that comment. My apologies. Can't update it already – wonderflame May 04 '23 at 23:44
  • 1
    deleted that comment, check the new one: since, `"future_value"` doesn't exist in all members of the union, the `K` isn't inferred to have it as a key. By writing `"future_value" & keyof K` I'm telling ts to take the intersection of these two, and if there is no `"future_value"` in `keyof K` the intersection will result `never` – wonderflame May 04 '23 at 23:45
  • Okay thank you. Can you review your original answer and include the information in this conversation? I want to make sure there are no typos in the answer I accept. Thanks again. – Daniel Kaplan May 04 '23 at 23:47
  • 1
    thanks, for noting all of the issues. Updated the answer – wonderflame May 04 '23 at 23:54