Alexander Weber
Alexander Weber

Reputation: 799

Check for required fields of an arbitrary input object

I use TypeScript in AWS Lambdas. Input objects provided for such Lambdas can be arbitrary. I always need to check if all required fields are provided in the input. I need to provide an error message explicitly saying which fields are missing if this is the case. If all required fields are provided, I need to cast the object into a domain object defined as an interface. Here an example of how this can be done:

interface Car {
  wheels: string
  doors: string
  engine: string
}

export const checkIfCar = (maybeCar: Partial<Car>): Car | { error: string } => {
  if (maybeCar.wheels == null) {
    return { error: 'Car should have wheels' };
  } else {
    if (maybeCar.doors == null) {
      return { error: 'Car should have doors' };
    } else {
      if (maybeCar.engine == null) {
        return { error: 'Car should have an engine' }
      } else {
        const car: Car = {
          wheels: maybeCar.wheels,
          doors: maybeCar.doors,
          engine: maybeCar.engine
        }
        return car
      }
    }
  }
}

While this works perfectly well, there is a lot of nesting and boiler plate happening. Is there a TypeScript native way to achieve the same, but in a more concise way? If not, is there a library which takes care of this pattern? I imagine this is a common issue not only I am dealing with.

I am aware of User defined type guards. However this requires that the guard function is updated each time a new field is added, which can easily be forgotten. It also does not solve the issue of telling me which fields are missing. I am looking for a way where all fields of an object are checked for existence automatically.

Upvotes: 1

Views: 1661

Answers (2)

cyco130
cyco130

Reputation: 4934

TypeScript has no builtin feature for runtime type checking or validation. I'm also guessing that your problem doesn't get solved with a simple !== null check. What if it's undefined? What if engine is an object and not a string?

Luckily there are several libraries you can use for runtime validation and some of them cooperate very nicely with TypeScript. One such library is runtypes. For your purposes, you can use it in this manner:

import * as runtypes from "runtypes";

const CarType = runtypes.Record({
  wheels: runtypes.String,
  doors: runtypes.String,
  engine: runtypes.String,
});

type Car = runtypes.Static<typeof CarType>;

Now you can use CarType.check(maybeCar).

Other alternatives are zod, io-ts, and class-validator, each with their particular ways of doing essentially the same thing.

For more complex validation tasks I've also used JSON schema + json-schema-to-typescript along with a JSON schema checker, such as AJV. It takes longer to setup but it is the ultimate validation + static typing tool in my opinion.

Upvotes: 2

Orelsanpls
Orelsanpls

Reputation: 23495

Possible answer using an automatical way to check.

Unfortunately you need to provide the keys of the properties to check.

snippet

interface Car {
  wheel: string;
  door: string;
  engine: string;
}

const CarKeys = [
  'wheel',
  'door',
  'engine',
];

type ErrorRet = { 
  error: string;
};

const checkIfCar = (maybeCar: {
  [key in keyof Car]?: string | null;
}): Car | ErrorRet => {
  return CarKeys.reduce((tmp, x) => {
    const key: keyof Car = x as keyof Car;

    // leaves automatically if we already found an error
    if ('error' in tmp) {
      return tmp;
    }
    
    // Check for the missing data
    if (maybeCar[key] === void 0 || maybeCar[key] === null) {
      return {
        error: `Car should have ${x}`,
      };
    }

    tmp[key] = maybeCar[key] as string;

    return tmp;
  }, {} as (Car | ErrorRet));
}

console.log(checkIfCar({
  wheel: '',
  door: '',
}));

console.log(checkIfCar({
  wheel: '',
  door: null,
  engine: '',
}));

console.log(checkIfCar({
  wheel: '',
  door: '',
  engine: '',
}));

Upvotes: 2

Related Questions