Reputation: 799
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
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
Reputation: 23495
Possible answer using an automatical way to check.
Unfortunately you need to provide the keys of the properties to check.
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