Say I have this type:
export interface Opts {
paths?: string | Array<string>,
path?: string | Array<string>
}
I want to tell the user that they must pass either paths or path, but it is not necessary to pass both. Right now the problem is that this compiles:
export const foo = (o: Opts) => {};
foo({});
does anyone know to allow for 2 or more optional but at least 1 is necessary parameters with TS?
This works.
It accepts a generic type
T
, in your case astring
.The generic type
OneOrMore
defines either 1 ofT
or an array ofT
.Your generic input object type
Opts
is either an object with either a keypath
ofOneOrMore<T>
, or a keypaths
ofOneOrMore<T>
. Although not really necessary, I made it explicit with that the only other option is never acceptable.There is an error with
{}
If you already have that interface defined and want to avoid duplicating the declarations, an option could be to create a conditional type that takes a type and returns a union with each type in the union containing one field (as well as a record of
never
values for any other fields to dissalow any extra fields to be specified)Edit
A few details on the type magic used here. We will use the distributive property of conditional types to in effect iterate over all keys of the
T
type. The distributive property needs an extra type parameter to work and we introduceTKey
for this purpose but we also provide a default of all keys since we want to take all keys of typeT
.So what we will do is actually take each key of the original type and create a new mapped type containing just that key. The result will be a union of all the mapped types that contain a single key. The mapped type will remove the optionality of the property (the
-?
, described here) and the property will be of the same type as the original property inT
(T[TKey]
).The last part that needs explaining is
Partial<Record<Exclude<keyof T, TKey>, never>>
. Because of how excess property checks on object literals work we can specify any field of the union in an object key assigned to it. That is for a union such as{ path: string | Array<string> } | { paths: string | Array<string> }
we can assign this object literal{ path: "", paths: ""}
which is unfortunate. The solution is to require that if any other properties ofT
(other thenTKey
so we getExclude<keyof T, TKey>
) are present in the object literal for any given union member they should be of typenever
(so we getRecord<Exclude<keyof T, TKey>, never>>
). But we don't want to have to explicitly specifynever
for all members so that is why wePartial
the previous record.You may use
To increase readability you may write: