In a recent project, I encountered a problem where we needed to validate an object with keys dynamically defined by a constant map and enforce that at least one key has a valid value.
The Challenge
We had a MetadataMap object that defined valid keys and their corresponding types:
const MetadataMap = {
userId: Number,
utmSource: String,
utmMedium: String,
utmCampaign: String,
} as const;
From this map, we needed to:
- Dynamically generate a TypeScript interface to enforce type safety.
- Create a Yup validation schema that validates the object based on the map.
- Ensure at least one key in the object has a valid, non-undefined value.
- Avoid hardcoding keys to make the solution maintainable.
But, TypeScript enforces static types at compile time, while Yup handles runtime validation.
Step 1: Generating the Interface
To generate the TypeScript interface from the MetadataMap, we used keyof and mapped types. Hereβs how we defined it:
type Metadata = {
[K in keyof typeof MetadataMap]: typeof MetadataMap[K] extends NumberConstructor
? number
: string;
};
This approach ensured that any updates to MetadataMap were automatically reflected in the Metadata interface. For example:
// Resulting Metadata interface:
interface Metadata {
userId?: number;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
}
Step 2: Dynamically Generating the Yup Schema
We needed to dynamically create a Yup schema that matched the keys and types in MetadataMap. Using Object.keys and a reducer, we mapped each key to its corresponding Yup validator:
const metadataSchema = Yup.object(
Object.keys(MetadataMap).reduce((schema, key) => {
const type = MetadataMap[key as keyof typeof MetadataMap];
if (type === Number) {
schema[key] = Yup.number().optional();
} else if (type === String) {
schema[key] = Yup.string().optional();
}
return schema;
}, {} as Record<string, any>)
);
This method eliminated hardcoding and ensured that changes in MetadataMap were reflected in the schema without manual updates.
Step 3: Adding the βAt Least One Keyβ Rule
The next challenge was ensuring that at least one key in the object had a defined value. We added a .test method to the Yup schema:
metadataSchema.test(
"at-least-one-key",
"Metadata must have at least one valid key.",
(value) => {
if (!value || typeof value !== "object") return false;
const validKeys = Object.keys(MetadataMap) as (keyof typeof MetadataMap)[];
return validKeys.some((key) => key in value && value[key] !== undefined);
}
);
This logic:
- Ensures the object is valid.
- Extracts valid keys dynamically from MetadataMap.
- Verifies that at least one key has a non-undefined value.
The Result
Hereβs how the final schema behaves:
const exampleMetadata = {
userId: undefined,
utmSource: "google",
extraField: "invalid", // This key is ignored.
};
metadataSchema
.validate(exampleMetadata)
.then(() => console.log("Validation succeeded"))
.catch((err) => console.error("Validation failed:", err.errors));
In this example, validation succeeds because utmSource is a valid key with a non-undefined value, even though userId is undefined and extraField is not part of MetadataMap.
Top comments (0)