If you have an existing interface defined and want to prevent duplicate declarations, one approach is to create a conditional type that takes a type and returns a union with each type containing a single field (along with a record of never
values for any additional fields to disallow specifying extra fields).
export interface Options {
paths?: string | Array<string>,
path?: string | Array<string>
}
type SingleField<T, Key extends keyof T = keyof T> =
Key extends keyof T ? { [Prop in Key]-?:T[Key] } & Partial<Record<Exclude<keyof T, Key>, never>>: never
export const handleOptions = (o: SingleField<Options>) => {};
handleOptions({ path : '' });
handleOptions({ paths: '' });
handleOptions({ path : '', paths:'' }); // error
handleOptions({}) // error
Additional Information:
A closer look at the type manipulation technique employed here. We utilize the distributive property of conditional types to essentially iterate over all keys of the T
type. The distributive property requires an extra type parameter to function, so we introduce Key
for this purpose with a default value of all keys since we aim to extract all keys from type T
.
Our goal is to derive individual mapped types for each key of the original type, resulting in a union of these mapped types, each encapsulating just one key. This process removes optionality from the property (indicated by -?
, detailed here) while maintaining the same data type as the corresponding property in T
(T[Key]
).
The final aspect worth elaborating on is
Partial<Record<Exclude<keyof T, Key>, never>>
. Due to how object literals undergo excess property checks, it's possible to assign any field of the union to an object key within an object literal. For instance, a union like
{ path: string | Array<string> } | { paths: string | Array<string> }
permits employing the object literal
{ path: "", paths: ""}</code, which isn't ideal. To remedy this issue, we mandate that if other properties of <code>T
(beyond
Key
, denoted by
Exclude<keyof T, Key>
) are included in the object literal of any union member, they must be of type
never
(hence using
Record<Exclude<keyof T, Key>, never>>
). By partially modifying the preceding record, explicit specification of
never
for every member is avoided.