TypeScript require one parameter or the other, but not neither

前端 未结 4 511
不思量自难忘°
不思量自难忘° 2021-01-04 01:50

Say I have this type:

export interface Opts {
  paths?: string | Array,
  path?: string | Array

        
相关标签:
4条回答
  • 2021-01-04 02:12

    You may use

    export type Opts = { path: string | Array<string> } | { paths: string | Array<string> }
    

    To increase readability you may write:

    type StringOrArray = string | Array<string>;
    
    type PathOpts  = { path : StringOrArray };
    type PathsOpts = { paths: StringOrArray };
    
    export type Opts = PathOpts | PathsOpts;
    
    0 讨论(0)
  • 2021-01-04 02:13

    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)

    export interface Opts {
        paths?: string | Array<string>,
        path?: string | Array<string>
    }
    
    type EitherField<T, TKey extends keyof T = keyof T> =
        TKey extends keyof T ? { [P in TKey]-?:T[TKey] } & Partial<Record<Exclude<keyof T, TKey>, never>>: never
    export const foo = (o: EitherField<Opts>) => {};
    foo({ path : '' });
    foo({ paths: '' });
    foo({ path : '', paths:'' }); // error
    foo({}) // error
    

    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 introduce TKey for this purpose but we also provide a default of all keys since we want to take all keys of type T.

    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 in T (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 of T (other then TKey so we get Exclude<keyof T, TKey>) are present in the object literal for any given union member they should be of type never (so we get Record<Exclude<keyof T, TKey>, never>>). But we don't want to have to explicitly specify never for all members so that is why we Partial the previous record.

    0 讨论(0)
  • 2021-01-04 02:14

    This works.

    It accepts a generic type T, in your case a string.

    The generic type OneOrMore defines either 1 of T or an array of T.

    Your generic input object type Opts is either an object with either a key path of OneOrMore<T>, or a key paths of OneOrMore<T>. Although not really necessary, I made it explicit with that the only other option is never acceptable.

    type OneOrMore<T> = T | T[];
    
    export type Opts<T> = { path: OneOrMore<T> } | { paths: OneOrMore<T> } | never;
    
    export const foo = (o: Opts<string>) => {};
    
    foo({});
    

    There is an error with {}

    0 讨论(0)
  • 2021-01-04 02:25

    You are basically looking for an exclusive union type.

    It has been already proposed but unfortunately, in the end, it was declined.

    I found the proposed solutions here not to my liking, mostly because I'm not a fan of fancy and complex types.

    Have you tried with function overloading?

    I was in a similar situation and for me, this was the solution.

    interface Option1 {
      paths: string | string[];
    }
    
    interface Option2 {
      path: string | string[];
    }
    
    function foo(o: Option1): void;
    function foo(o: Option2): void;
    function foo(o: any): any {}
    
    foo({ path: './' });
    foo({ paths: '../' });
    // The folling two lines gives an error: No overload matches this call.
    foo({ paths: './', path: '../' });
    foo({})
    
    

    With arrow function the same code as above would instead be:

    interface Option1 {
      paths: string | string[];
    }
    
    interface Option2 {
      path: string | string[];
    }
    
    interface fooOverload {
      (o: Option1): void;
      (o: Option2): void;
    }
    
    const foo: fooOverload = (o: any) => {};
    
    foo({ path: '2' });
    foo({ paths: '2' });
    // The following two lines gives an error: No overload matches this call.
    foo({ paths: '', path: 'so' });
    foo({});
    

    Hope this helps you out!

    0 讨论(0)
提交回复
热议问题