TypeScript doesn't have built-in functionality that represents exhaustive arrays like this. You could try to write a type which is the union of all possible tuples which meet your criteria, but this will not scale well for moderately sized unions, and might not even be tractable for small cases if you want to allow for duplicate entries.
If I really wanted to do this, I'd be inclined to write a helper function that would try to infer the field names from the array and then use a conditional type that causes a compiler error if those field names do not exhaust keyof T. This is possibly fragile and definitely complicated, but here is one possible implementation:
interface FieldNamed<K extends PropertyKey> {
name: K,
label: string
}
const exhaustiveFieldArray = <T extends object>() => <K extends keyof T>(
...fields: [FieldNamed<K>, ...FieldNamed<K>[]] &
(keyof T extends K ? unknown : FieldNamed<Exclude<keyof T, K>>[])
): Field<T>[] => fields;
const exhaustiveItemFieldArray = exhaustiveFieldArray<Item>();
The function exhaustiveFieldArray<T>() takes a manually-specified type parameter T and returns a new function which accepts a variadic number of arguments of type Field<T>, and complains if it can't be sure that you included all field names.
Let's make sure that it works before we try to explain how it works:
const fields = exhaustiveItemFieldArray(
{ name: 'description', label: 'Description' },
{ name: 'location', label: 'location' }
); // okay
const badFields = exhaustiveItemFieldArray(
{ name: 'description', label: 'Description' },
{ name: 'locution', label: 'location' } // error!
//~~~~ <-- Type '"locution"' is not assignable to type 'keyof Item'
)
const badFields2 = exhaustiveItemFieldArray(
{ name: 'description', label: 'Description' } // error!
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"description"' is not assignable to type '"location"'.
)
const badFields3 = exhaustiveItemFieldArray(); // error!
// expected at least one argument
const badFields4 = exhaustiveItemFieldArray(
{ name: 'location', label: 'location' },
{ name: 'location', label: 'location' }
) // error!
This all looks okay to me. If we are missing fields, we get errors.
Here's a sketch of how it works. The returned function is generic in the type parameter K extends keyof T. The fields rest parameter is the intersection of two types. The first one is used to infer what was passed in:
[FieldNamed<K>, ...FieldNamed<K>[]]
That means it is an array of at least one element (it's a tuple of one element followed by some number of other elements), each of which must be of type FieldNamed<K> for the inferred K. This will end up making K the union of all field names included.
The second type is used to check that K is exhaustive of keyof T. We already know K extends keyof T, but we want to make sure that keyof T extends K also:
& (keyof T extends K ? unknown : FieldNamed<Exclude<keyof T, K>>[])
This conditional type checks keyof T extends K. If it is true, then everything is fine, and we return unknown. Intersecting with unknown is a no-op (XYZ & unknown is equivalent to XYZ), so that doesn't prevent anything from compilng. If it is false, then we have a problem, and we return FieldNamed<Exclude<keyof T, K>>[]. The Exclude<T, U> utility type removes elements from a union; so Exclude<keyof T, K> gives us those keys we left out. And so we are intersecting the actual array type with an array of fields which we missed. This will result in compiler errors complaining about the fact that you missed things. The errors might not be the most comprehensible, but at least there are errors.
So, hooray? It works as far as it goes, but I don't know if it's worth it to you. You might instead consider changing your data structure from an array (which is hard for the compiler to check) to an object whose keys are the same as T (which is easy for the compiler to check). But this answer is already long so I'm not going to expand the scope further to show how such a thing would be implemented. 😅
Playground link to code