How can I require a specific combination of parameters (i.e. omit required alias names)?
Solution 1:
My inclination here would be to make a type function called OmitSuperfluousProperties<T>
which would take a union type T
and produce a new union where each member explicitly forbids extra keys from the other members. So OmitSuperfluousProperties<{a: string} | {b: number}>
should become something like {a: string, b?: never} | {a?: never, b: number}
.
Here's one possible definition:
type OmitSuperfluousProperties<T, K extends PropertyKey = AllKeys<T>> =
T extends any ? (
T & Partial<Record<Exclude<K, keyof T>, never>>
) extends infer O ? { [P in keyof O]: O[P] } : never
: never;
type AllKeys<T> = T extends any ? keyof T : never;
In this I'm using distributive conditional types to split T
up into its union members, operate on each member, and unite them back together.
AllKeys<T>
is a conditional type that takes a union type T
and returns the full set of keys for all members. So AllKeys<{a: string} | {b: number}>
is "a" | "b"
. Then this is given as a default second argument to OmitSuperfluousProperties<T, K>
. For each member of T
, we intersect it with Partial<Record<Exclude<K, keyof T>, never>>
. In the case where T
is {a: string} | {b: number}
and K
is "a" | "b"
, this becomes {a: string} & {b?: never}
for the first member and {b: number} & {a?: never}
for the second member. And finally I use a conditional type inference trick to turn the intersections into single object types, so {a: string} & {b?: never}
becomes the equivalent and easier-to-look-at {a: string, b?: never}
.
Let's try it:
type StrictLocation = OmitSuperfluousProperties<Location>
/* type StrictLocation = {
latitude: number;
longitude: number;
lng?: undefined;
lat?: undefined;
} | {
latitude: number;
lng: number;
longitude?: undefined;
lat?: undefined;
} | {
lat: number;
longitude: number;
latitude?: undefined;
lng?: undefined;
} | {
lat: number;
lng: number;
latitude?: undefined;
longitude?: undefined;
} */
That looks like the type you want, right? We can check:
formatLocation({
lat: 123.456,
longitude: -12.3456
}); // okay
formatLocation({
lat: 123.456,
latitude: 123.456,
longitude: -12.3456
}); // error!
/* Argument of type '{ lat: number; latitude: number; longitude: number; }'
is not assignable to parameter of type
'{ latitude: number; longitude: number; lng?: undefined; lat?: undefined; } |
{ latitude: number; lng: number; longitude?: undefined; lat?: undefined; } |
{ lat: number; longitude: number; latitude?: undefined; lng?: undefined; } |
{ ...; }'. */
Good news: you get an error in the case where you specify both latitude
and lat
. Bad news: the error is not particularly enlightening if you don't already know what you're looking for. The compiler sees that the value passed in is not assignable to any of the four members of the union, but it doesn't really know which failure is the important one to tell you about. A human being would say "you can't use both latitude
and lat
here", but the compiler says "you left out lng
maybe"? And if you add lng
then it says "oh, wait, one of these should be undefined
, I think", which is closer to helpful, but still sub-optimal. Oh well, at least it's a type compatibility error!
Anyway, hope that helps. Good luck!
Playground link to code