It presents a challenge to restrict this on the function side. Finding a universal solution seems unlikely.
Non-conventional approach: using function overload
interface Data<TObject> {
object: TObject
key: keyof TObject
}
function manipulate<T1, T2, T3, T4>(items: [Data<T1>, Data<T2>, Data<T3>, Data<T4>]): void
function manipulate<T1, T2, T3>(items: [Data<T1>, Data<T2>, Data<T3>]): void
function manipulate<T1, T2>(items: [Data<T1>, Data<T2>]): void
function manipulate<T1>(items: Data<T1>[]): void {
}
manipulate([
{ object: { a: '1' }, key: 'a' },
{ object: { b: '1' }, key: 'b' },
{ object: { c: '1' }, key: 'a' }, // not allowed
])
Alternative method: ensuring types on the calling end
Essentially, you depend on a utility function. There is still room for error here that may go unnoticed by the compiler (refer to the last item in the example)
interface Data<TObject extends object> {
object: TObject
key: keyof TObject
}
function manipulate(data: Data<any>[]) {
}
function createData<T extends object>(object: T, key: keyof T): Data<T> {
return {
object,
key
}
}
manipulate([
createData({ a: 1 }, 'a'),
createData({ b: 2 }, 'f'), // not allowed
{ object: { b: 2 }, key: 'f' }, // allowed
])
Method 3: forming a processor entity with a generic add function
interface Data<TObject> {
object: TObject
key: keyof TObject
}
function establishProcessingArray() {
const array: Data<any>[] = []
return {
add<T>(item: Data<T>) {
array.push(item)
return this
},
result() {
// process the array and provide the outcome
}
}
}
const result = establishProcessingArray()
.add({ object: { a: '1' }, key: 'a' })
.add({ object: { b: '1' }, key: 'b' })
.add({ object: { c: '1' }, key: 'a' }) // not allowed
.result()