No matter how you define ObjectWithIcon<T>
, it's best to avoid using the type ObjectWithIcon<MyObject>[]
. This is simply a plain array type that doesn't consider whether its elements have a specific property or not. It only checks if each element adheres to the structure of ObjectWithIcon<MyObject>
without looking at the overall array.
Instead, consider utilizing a union of array types. This means specifying that the array can be either one containing MyObject
s with an iconName
, or one containing MyObject
s without an iconName
.
To achieve this, you can create a type like:
type ObjectsAllWithOrAllWithoutIcon = {
iconName: string; // Required property
label: string;
}[] | {
iconName?: never; // Prohibited property
label: string;
}[]
While TypeScript doesn't directly support "without a given property", it does allow for declaring properties as optional with the impossible `never` type, which provides a similar outcome.
For this purpose, we can define utility types representing "with", "without", and the "either-or" scenario within the arrays:
type With<T, K extends keyof T> = T & Required<Pick<T, K>>;
type Without<T, K extends keyof T> = T & Partial<Record<K, never>>;
type ArrayAllWithOrAllWithout<T, K extends keyof T> =
With<T, K>[] | Without<T, K>[]
The With<T, K>
type indicates that the object includes all properties from type T
, plus the required property specified by K
. On the other hand, Without<T, K>
signals that the object possesses all properties from type T
, with the option to exclude the property indicated by K
through the use of the `never` type.
Finally, the
ArrayAllWithOrAllWithout<T, K>
denotes an array containing objects that are either instances of
With<T, K>
, or objects following the structure of
Without<T, K>
.
This structure ensures that:
interface MyObject {
iconName?: string;
label: string;
}
type ObjectsAllWithOrAllWithoutIcon =
ArrayAllWithOrAllWithout<MyObject, "iconName">;
is equivalent to the manually defined ObjectsAllWithOrAllWithoutIcon
mentioned earlier. You can then verify that your examples perform as intended:
const arrayOfObjects: ObjectsAllWithOrAllWithoutIcon = [ // error!
// ~~~~~~~~~~~~~~
// Property 'iconName' is missing in type '{ label: string; }'
// but required in type 'Required<Pick<MyObject, "iconName">>'.
{ iconName: "box", label: "storage" },
{ label: "memory" },
];
const arrayOfObjects2: ObjectsAllWithOrAllWithoutIcon = [
{ iconName: "box", label: "storage" },
{ iconName: "circle", label: "memory" },
]; // okay
const arrayOfObjects3: ObjectsAllWithOrAllWithoutIcon = [
{ label: "storage" },
{ label: "memory" },
]; // okay
Playground link to code