DEV Community

Cover image for Dynamically Generating Interfaces and Validation Schemas in TypeScript with Yup
Saurabh Saha
Saurabh Saha

Posted on

Dynamically Generating Interfaces and Validation Schemas in TypeScript with Yup

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:

  1. Dynamically generate a TypeScript interface to enforce type safety.
  2. Create a Yup validation schema that validates the object based on the map.
  3. Ensure at least one key in the object has a valid, non-undefined value.
  4. 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:

  1. Ensures the object is valid.
  2. Extracts valid keys dynamically from MetadataMap.
  3. 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)