Home > OS >  An array of object with specific type using generics with typescript
An array of object with specific type using generics with typescript

Time:06-07

I want to have an array of objects of the following type (arbitrary length):

[
  {p1: T,  p2: T}, 
  {p1: T2, p2: T2}
  ...
]

For a single object, this works as I want it to:

type O<T> = {
  p1: T;
  p2: T;
};


const f = <T,>(a: O<T>) => { 
  console.log(a) 
}

f({ p1: 1, p2: 2 }); // makes sure p1 and p2 has the same type

I want to pass to the function f an array of objects, and I want each object individually to have the same type constraint.

I tried:

O<T> = {
  p1: T;
  p2: T;
}

const f = <T,>(a: O<T>[]) => { 
  console.log(a) 
}

f([
  { p1: 1, p2: 2 },
  { p1: '3', p2: '4' }, // Type 'string' is not assignable to type 'number'
]);

I also tried:

type O<T> = {
  p1: T;
  p2: T;
};

type A<T> = {
  [P in keyof T]: O<T[P]>;
};

const f = <T,>(a: A<T>) => { 
  console.log(a) 
}

f([
  { p1: 1, p2: 2 },
  { p1: 3, p2: '4' }, // allows but I don't want it to be allowed p1 != p2 unfortunatly T resolvs to string | number
]);

I want O<T> to be applied for each object in the array individually. I tried so many things, that at this point I'm not sure if it is possible to achieve this with typescript.

I want this to work for any custom type not only number | string.

Any help would be much appreciated.

CodePudding user response:

I think you had it at the outset ;-)

type O<T1, T2> = [ { p1: T1, p2: T1 }, { p3: T2, p4: T2 } ]

function f<T1,T2>(a: O<T1, T2>) { 
  console.log(a) 
}

f([
  { p1: 1, p2: 2 },
  { p3: '3', p4: '4' }
]);

f<number, string>([
  { p1: 1, p2: 2 },
  { p3: '3', p4: '4' }
]);

f<number, string>([
  { p1: 1, p2: 2 },
  { p3: 3, p4: '4' }
  //^^ Type 'number' is not assignable to type 'string'
]);

Part of the issue with correctly warning on {p3: 3, p4: '4'} without an explicit type, the generic f is inferring the type based on the input argument. It's telling you that the second object cannot have mismatched types, but they could be the same as the first objects type (T1 and T2 could legitimately be the same type).

You could make the generic conditional, so that T2 cannot extend T1 but that might be restrictive with objects without knowing the shape of them.

CodePudding user response:

In order to do that, we need to infer each element from the list and check whether p1 and p2 have same types.

To infer each element from list we need to use variadic tuple types. Apart from that , we need to iterate through each element an check p1 and p2.

See this:

type BothEqual<T extends Base<any>> =
    T['p1'] extends T['p2']
    ? (T['p2'] extends T['p1']
        ? T
        : never)
    : never

type Validation<
    Arr extends Array<unknown>,
    Result extends Array<unknown> = []
    > =
    Arr extends []
    ? []
    : (Arr extends [infer H extends Base<any>]
        ? [...Result, BothEqual<H>]
        : (Arr extends [infer Head extends Base<any>, ...infer Tail]
            ? Validation<[...Tail], [...Result, BothEqual<Head>]>
            : Readonly<Result>)
    )

BothEqual just checks whether p1 and p2 have same type.

Validation - iterates through eack element in the list and calls BothEqual on it. Hence, if elem is valid it returns this element otherwise it returns never:

interface Base<T> {
    p1: T,
    p2: T
};


type BothEqual<T extends Base<any>> =
    T['p1'] extends T['p2']
    ? (T['p2'] extends T['p1']
        ? T
        : never)
    : never


type Validation<
    Arr extends Array<unknown>,
    Result extends Array<unknown> = []
    > =
    /**
     * If Arr is empty tuple, return empty result
     */
    Arr extends []
    ? []
    /**
     * If Arr is a tuple with one element, stop recursion.
     * See TS 4.7 https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#extends-constraints-on-infer-type-variables docs
     * for [infer H extends Base<any>] syntax
     * 
     */
    : (Arr extends [infer H extends Base<any>]
        ? [...Result, BothEqual<H>]
        /**
         * If Arr has more than one element - call it recursively
         */
        : (Arr extends [infer Head extends Base<any>, ...infer Tail]
            ? Validation<[...Tail], [...Result, BothEqual<Head>]>
            : Readonly<Result>)
    )


function f<T, List extends Base<T>[]>(a: [...List] & Validation<[...List]>) {
    console.log(a)
}

f([
    { p1: 1, p2: 2 },
    { p1: 3, p2: '42' } // error
]);

Playground As you might have noticed, second element in provided argument is highlighted. Also, you can prioritize p1 over p2. It mean that p1 is the source of true. Then, if p2 is invalid only p2 will be highlighted. Try this:

type BothEqual<T extends Base<any>> =
    T['p2'] extends T['p1']
    ? T
    : Omit<T, 'p2'> & { p2: never }

You can simplify Validation type:

type Validation<
    Arr extends Array<unknown>,
    > =
    {
        [Elem in keyof Arr]: Arr[Elem] extends Base<any> ? BothEqual<Arr[Elem]> : never
    }

But then all elements in the array will be highlighted which might be hard to understand what went wrong.

If you are interested in TS function argument validation and tuple iteration you can check these articles from my blog:

  • Related