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",
}
CodePudding user response:
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!
CodePudding user response:
Try this:
type Bar = {k1: string, k2?: never} | {k1?: never, k2: string};