Skip to content
Advertisement

Typescript typecast object so specific required keys are no longer optional in the type?

Say you have an object type:

Type Person = {
  name?: string;
  color?: string;
  address? string;
}

However you want to change that type into the following, where you know name and color will exist.

Type Person = {
  name: string;
  color: string;
  address? string;
}

Therefore, there is the function

const throwIfUndefined = (
  object: {[key: string]: any},
  requiredKeys: string[]
): ReturnTypeHere => {
  for (const key of requiredKeys) {
    if (!object[key]) throw new Error("missing required key");
  }

  return object;
};

What is the proper way to type the params of the function as well as the return type (ReturnTypeHere)? Written correctly, the below will either 1) throw error 2) console log the name. It will never console log undefined.

const person = {...}

const requiredKeys = ["name", "color"];
const verifiedPerson = throwIfUndefined(person, requiredKeys);
console.log(verifiedPerson.name)

Advertisement

Answer

If you have an object type T and a union of its keys K that you’d like to have required, you can write RequireKeys<T, K> like this:

type RequireKeys<T extends object, K extends keyof T> =
  Required<Pick<T, K>> & Omit<T, K>;

Here we are using the Required<T>, Pick<T, K>, and Omit<T, K> utility types. There are probable edge cases here, such as if T has a string index signature and string appears inside K, but to a first approximation it should work.

It’s also a little difficult to understand what RequiredKeys<Person, "name" | "color"> is from how it’s displayed in your IDE:

type VerifiedPerson = RequireKeys<Person, "name" | "color">;
// type VerifiedPerson = Required<Pick<Person, "name" | "color">> & 
//   Omit<Person, "name" | "color">

If you want the compiler to be a little more explicit, you can do something like the following to expand the type into its properties:

type RequireKeys<T extends object, K extends keyof T> =
  (Required<Pick<T, K>> & Omit<T, K>) extends
  infer O ? { [P in keyof O]: O[P] } : never;

which results in

/* type VerifiedPerson = {
    name: string;
    color: string;
    address?: string | undefined;
} */

which is easier to see.

You then need to make throwIfUndefined() a generic function so the compiler can keep track of the relationship between the object and requiredKeys passed in:

const throwIfUndefined = <T extends object, K extends keyof T>(
  object: T,
  requiredKeys: readonly K[]
) => {
  for (const key of requiredKeys) {
    if (!object[key]) throw new Error("missing required key");
  }
  return object as unknown as RequireKeys<T, K> // need to assert this
};

And to test:

const person: Person = {
  ...Math.random() < 0.8 ? { name: "Alice" } : {},
  ...Math.random() < 0.8 ? { color: "Color for a person is problematic" } : {}
};
const requiredKeys = ["name", "color"] as const;
const verifiedPerson = throwIfUndefined(person, 
  requiredKeys); // possible runtime error here
// const verifiedPerson: RequireKeys<Person, "name" | "color">

If you want the compiler to remember that the literal types "name" and "color" are members of requiredKeys then you need to do something like a const assertion (i.e., as const) to tell it so. Otherwise requiredKeys would just be string[] and you’d get weird/wrong results (we could guard against these but it would be possibly out of scope here).

And now, the compiler understands that name and color are defined, whereas address is still optional:

console.log(verifiedPerson.name.toUpperCase() + ": " +
  verifiedPerson.color.toUpperCase()); // no compile error
// [LOG]: "ALICE: COLOR FOR A PERSON IS PROBLEMATIC"

verifiedPerson.address // (property) address?: string | undefined

Playground link to code

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement