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: