Reputation: 319
Consider a function that's called like:
func([
{object: object1, key: someKeyOfObject1},
{object: object2, key: someKeyOfObject2}
])
It's got an array. I want to enforce that the key
field holds a key of the object that is held in object
. Every object can have a different shape.
Constructing such a type for a single object is easy:
type Type<T> = { obj: T, key: keyof T }
However, I don't know how to create an array out of it, where every single element would be enforced. Type<any>[]
would all drop types.
Upvotes: 1
Views: 566
Reputation: 33041
It is doable without extra functions but with small type overhead:
type Entity<Obj, Key> = {
object: Obj,
key: Key
}
type IsValid<T extends Entity<any, any>[]> =
/**
* Infer each element of the array
*/
T[number] extends infer Elem
/**
* Check if every element of the array extends Entity
*/
? Elem extends Entity<any, any>
/**
* Check if keyof Elem['object'] extends `key` property
* 1) if [key] property is one of object properties - return true
* 2) if at least one element does not meet your requirements return false | true,
* because some element are ok
*/
? keyof Elem['object'] extends Elem['key']
? true
: false
: false
: false;
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type Validator<T extends boolean> =
/**
* If IsValid returns false | true (boolean) it means Error
* otherwise - ok
*/
IsUnion<T> extends true ?
['Dear developer, please do smth right']
/**
* I'm using empty array here, because
* (...flag:[])=>any evaluates to function without arguments
*/
: []
const foo = <
Value extends Record<PropertyKey, string>,
Key extends keyof Value,
Data extends Entity<Value, Key>[],
>(a: [...Data], ...flag: [...Validator<IsValid<[...Data]>>]) => a
/**
* Ok
*/
foo([{
object: { name: 'John' }, key: 'name'
},
{
object: { surname: 'John' }, key: 'surname'
}])
/**
* Error
*/
foo([{
object: { name: 'John' }, key: 'name'
},
{
object: { surname: 'John' }, key: 'name'
}])
This solution consists of two parts:
Part 1
We need to infer ach element of the array with help of variadic tuple types - Data
generic. Here, in my article, you can find an explanation how to do it.
Part 2
We need to check if every element meets your requirements: Validator
and IsValid
type utils. More about this technique you can find in my blog here and here
Upvotes: 1
Reputation: 616
Restricting this on the function side is difficult. I don't even think there's a generic way to do that.
Non-generic solution: function overload
interface Item<TObject> {
object: TObject
key: keyof TObject
}
function func<T1, T2, T3, T4>(items: [Item<T1>, Item<T2>, Item<T3>, Item<T4>]): void
function func<T1, T2, T3>(items: [Item<T1>, Item<T2>, Item<T3>]): void
function func<T1, T2>(items: [Item<T1>, Item<T2>]): void
function func<T1>(items: Item<T1>[]): void {
}
func([
{ object: { a: '1' }, key: 'a' },
{ object: { b: '1' }, key: 'b' },
{ object: { c: '1' }, key: 'a' }, // not allowed
])
Solution 2: enforcing types on the calling side Basically, you rely on utility function. There's still way to make an error here and have compiler miss it (see last item in the example)
interface Item<TObject extends object> {
object: TObject
key: keyof TObject
}
function func(items: Item<any>[]) {
}
function createItem<T extends object>(object: T, key: keyof T): Item<T> {
return {
object,
key
}
}
func([
createItem({ a: 1 }, 'a'),
createItem({ b: 2 }, 'f'), // not allowed
{ object: { b: 2 }, key: 'f' }, // allowed
])
Solution 3: create a processor object with generic add method
interface Item<TObject> {
object: TObject
key: keyof TObject
}
function createMyArrayProcessor() {
const array: Item<any>[] = []
return {
add<T>(item: Item<T>) {
array.push(item)
return this
},
result() {
// process the array here and return the result
}
}
}
const result = createMyArrayProcessor()
.add({ object: { a: '1' }, key: 'a' })
.add({ object: { b: '1' }, key: 'b' })
.add({ object: { c: '1' }, key: 'a' }) // not allowed
.result()
Upvotes: 1