2

The below code allows to create an oject that contains "k1" or "k2" or ("k1" and "k2") or none of them.

type Foo = "k1"|"k2";

type Bar = {[key in Foo]?: string};

const myObj1: Bar = { // ok

}

const myObj3: Bar = { // ok
    "k1": "random string",
    "k2": "random string",
}

My goal is to be able to have one and only one mandatory key, either "k1" or "k2".

So i want to be able to do the below:

const myObj1: Bar = { // Compile error

}

const myObj2: Bar = { // Compile error
    "k1": "random string",
    "k2": "random string",
}

const myObj3: Bar = { // ok
    "k1": "random string",
}

const myObj4: Bar = { // ok
    "k2": "random string",
}
Manolis P.
  • 195
  • 2
  • 14
  • 1
    Does [this approach](https://tsplay.dev/mbA9dN) meet your needs? If not, what am I missing? – jcalz Feb 26 '22 at 14:37
  • 1
    Note that this question seems to be a combination of [this one](https://stackoverflow.com/q/48230773/2887218) and [this one](https://stackoverflow.com/q/46370222/2887218). – jcalz Feb 26 '22 at 14:41
  • Please clarify if you need to compute `Bar` programmatically from `Foo`, or if you would accept a solution where `Bar` does not depend on `Foo` at all. – jcalz Feb 26 '22 at 14:50
  • @jcalz Thank you for you answer. I had in mind what you post first. But what you mean "compute Bar programmatically from Foo" can you give my an example. – Manolis P. Feb 26 '22 at 14:52
  • If you add a key to `Foo`, (e.g., `type Foo = "k1"|"k2"|"k3";`) do you want `Bar` automatically to update itself? Or are you satisfied with [this answer](https://stackoverflow.com/a/71277510/2887218) where you'd have to manually update `Bar` to reflect the new key? – jcalz Feb 26 '22 at 14:54
  • 1
    I see, so your solution automatically update the Bar right? My scenario is simple for now because i had 4 possible keys and the solution from @Majed will work, but is a bit overkill every time that you add something to Foo to update the Bar mannually. – Manolis P. Feb 26 '22 at 14:57
  • Yes, I'll write up an answer. – jcalz Feb 26 '22 at 14:57
  • Sure, thank you. To be honest the way you provide is awesome, but is a bit complicate for someone that knows the basics of typescript. – Manolis P. Feb 26 '22 at 14:59

2 Answers2

1

Try this:

type Bar = {k1: string, k2?: never} | {k1?: never, k2: string};
Majed Badawi
  • 27,616
  • 4
  • 25
  • 48
1

In order for this to work as you desire, you need Bar to be a union type where each member in the union has one required property whose key is from Foo and whose value is of type string and where the rest of the keys from Foo are prohibited. TypeScript doesn't really let you prohibit a key (at least without enabling the --exactOptionalPropertyTypes compiler option), but making them optional properties of the never type is very close: For example, {x?: never} means that the x property must either be missing, or that it must be present and have a value of type never (which is impossible) or undefined (which is automatically added to optional properties). Anyway, that means for the particular Foo in your question, you want Bar to be:

type Bar = {
    k1: string;
    k2?: never;
} | {
    k2: string;
    k1?: never;
}

But presumably you want to compute Bar from Foo, so that if Foo changes, Bar will automatically update to match it. Here's one way to do it:

type Bar = { [K in Foo]:
  { [P in K]: string } & { [P in Exclude<Foo, K>]?: never }
}[Foo];

This is using a mapped type into which we immediately index to get a union. We can call this a "distributive type". Any approach that looks like {[K in XXX]: YYY<K>}[XXX], where XXX is some union type K1 | K2 | K3, will create a distribute type of the form YYY<K1> | YYY<K2> | YYY<K3>.

In the above type, we are therefore computing the union of { [P in K]: string } & { [P in Exclude<Foo, K>]?: never } for each K in Foo. So it will become ({ [P in "k1"]: string } & { [P in Exclude<Foo, "k1">]?: never }) | ({ [P in "k2"]: string } & { [P in Exclude<Foo, "k2">]?: never })

Let's look at just the first bit of that:

{ [P in "k1"]: string } & { [P in Exclude<Foo, "k1">]?: never }

The left half is equivalent to {k1: string} (since we're mapping over just one key) and the right have is equivalent to {k2?: never} because the Exclude<T, U> utility type filters out union members. These two halves are intersected together, so {k1: string} & {k2?: never}, which is equivalent to {k1: string, k2?: never} (but not represented that way).

So then the above will produce a type

/* type Bar = ({
    k1: string;
} & {
    k2?: never;
}) | ({
    k2: string;
} & {
    k1?: never;
}) */

which works as you want, but is kind of ugly with all those intersections.

You can get the compiler to collapse an intersection of object types to a single object type by doing a "no-op" mapped type. If O is {a: 1} & {b: 2}, then {[P in keyof O]: O[P]} will be {a: 1, b: 2}. So we can extend Bar's definition to do that:

type Bar = { [K in Foo]:
  { [P in K]: string } & { [P in Exclude<Foo, K>]?: never } extends
  infer O ? { [P in keyof O]: O[P] } : never
}[Foo];

What this does is copy { [P in K]: string } & { [P in Exclude<Foo, K>]?: never } into a new type parameter O, and map over it with {[P in keyof O]: O[P]} to collapse the intersection. Oh, and I'm using conditional type inference to get the copying done. Anything of the form XXX extends infer O ? YYY<O> : never will be equivalent to YYY<XXX>.

So finally, with this definition, we get:

/* type Bar = {
    k1: string;
    k2?: never;
} | {
    k2: string;
    k1?: never;
} */

Let's make sure that works how you want:

const myObj1: Bar = { // Compile error
}

const myObj2: Bar = { // Compile error
  "k1": "random string",
  "k2": "random string",
}

const myObj3: Bar = { // ok
  "k1": "random string",
}

const myObj4: Bar = { // ok
  "k2": "random string",
}

And let's make sure it updates when you add a key to Foo:

type Foo = "k1" | "k2" | "k3";        

/* type Bar = {
  k1: string;
  k2?: never;
  k3?: never;
} | {
  k2: string;
  k1?: never;
  k3?: never;
} | {
  k3: string;
  k1?: never;
  k2?: never;
} */

Looks good!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    Thank you for your the detailed explaination! I want to ask you the following: type Bar = {...}[Foo]; How that called [Foo]? From my understanding we need it because inside the {} we use the Foo. But why this is need it? – Manolis P. Feb 26 '22 at 16:03
  • 1
    If we didn't use it, then `Bar` would be `{k1: {k1: string, k2?: never}, k2: {k2: string, k1?: never}}` which is not what you want. A type like `T[K]` is an [indexed access type](//www.typescriptlang.org/docs/handbook/2/indexed-access-types.html), the type when you read a property by indexing into an object of type `T` with a key of type `K`. If `T` is `{a: 0, b: 1}` and `K` is `"a"` then `T[K]` is `0`. If `K` is `"a" | "b"`, then `T[K]` is `0 | 1`. With `{[K in Foo]: F}[Foo]` you will end up with `F<"k1"> | F<"k2"> | ...` (for all the members in `Foo`). – jcalz Feb 26 '22 at 17:53