Here's a generic solution that will work for any type and any field.
type RequiredField<T, Key extends keyof T> = Omit<T, Key> & Required<Pick<T, Key>>;
function isFieldsDefined<T extends {}, U extends [keyof T, ...Array<keyof T>]>(
obj: T,
keys: U
): obj is RequiredField<T, U[number]> {
return keys.every((key) => key in obj);
}
isFieldsDefined
takes 2 parameters:
- the object of type
T
- a non empty array (one or more items) of properties that exists on interface
T
. keyof
is T translates to bar | baz | buzz
. This is great because if you tried to check for a field which doesn't exist on the interface (booz
for instance), compilation will fail
- for every key, we check that the key exists in the object
The type guard used by this function is an interesting one. But before I explain it, there's a few things to know.
Omit
is a utility that creates a new type without the specified properties. Example:
Omit<Foo, "bar"> === interface {baz: number, buzz?: string;}
Pick
is a utility that creates a new type with the specified properties only. Example:
Pick<Foo, "bar"> === interface {bar: string;}
Required
is a utility that creates a new type where all the properties are required. Example:
Required<Foo> === interface {bar: string; baz: number; buzz: string;}
Hence, RequiredField
creates a new type where the specified fields are first omitted, then added back as non-undefineable. Below is how RequiredField<Foo, "buzz>
transitions into the new type:
1. RequiredField<Foo, "buzz">
2. Omit<Foo, "buzz"> & Required<Pick<Foo, "buzz">>
3. {bar: string; baz: number;} & Required<{buzz?: string}>
4. {bar: string; baz: number;} & {buzz: string}
5. {bar: string; baz: number; buzz: string}
Now that we know how RequiredField
works, we may have a second look at isFieldsDefined
type guard obj is RequiredField<T, U[number]>
.
Since type U
is an array of properties of T
, U[number]
will be a union type where each member corresponds with a value at an index in the array. Example:
type Fields = ["bar", "baz", "buzz"];
Fields[number] === "bar" | "baz" | "buzz"
Therefore, here's a break down of the type guard for isFieldsDefined(item, ["buzz"])
:
obj is RequiredField<Foo, ["buzz"][number]>
obj is RequiredField<Foo, "buzz">
obj is {bar: string; baz: number; buzz: string;}
You may check that multiple fields are defined too: isFieldsDefined(item, ["buzz", "bar"]);
"buzz" in item
? – Satellite