DEV Community

János Kukoda
János Kukoda

Posted on

Future-Proofing Code: Type-Safe KeyPath and Value Validation for Evolving Client-Backend Systems

If we have these three business entities in TypeScript:

export interface UserDTO {
    teams: { [teamId: string]: TeamDTO };
}

export interface TeamDTO {
    transformations: { [transformationId: string]: Transformation };
}

export interface Transformation {
    name: string;
    texts: { [id: string]: string };
}
Enter fullscreen mode Exit fullscreen mode

To modify these, we follow the command pattern. The modification parameters are wrapped in the value, and the key path indicates where the modification occurs. The type of modification here is represented by the setValue operation.

The advantage of this approach is that a single abstract method can handle all future modifications on the client-side. On the backend, if you're using a document-based database with a similar representation of business entities as on the client-side, the modification can also be implemented with a single endpoint.

This approach is strongly type-safe.

We perform two operations: the first duplicates a transformation and sets a new transformation in an object, and the second example overrides the transformation's name.

Type safety appears in two ways:

  1. If the keyPath points to an invalid location, a static error is thrown.
  2. If the value doesn’t match the type expected at the keyPath, a type error is generated.

For example, in TypeScript, value assignments can be triggered like this:

<button
    onClick={async () => {
        const id = randomString(4);
        const _transformation = {
            ...transformation,
            name: transformation?.name + ' copy',
            texts: {}
        };
        await auth.updateClientAndServerArray([
            {
                keyPath: ['teams', 'response', 'transformations', id],
                value: _transformation,
                operation: 'setValue',
            },
        ]);
    }}
>
    Duplicate transformation
</button>
Enter fullscreen mode Exit fullscreen mode

To update just the name field:

await auth.updateClientAndServerArray([
    {
        keyPath: ['teams', 'response', 'transformations', id, 'name'],
        value: 'some-name',
        operation: 'setValue',
    },
]);
Enter fullscreen mode Exit fullscreen mode

Type Definitions

These two types are used to ensure type safety. The first recursively traverses the business entities to verify whether the provided key path is valid:

export type KeyPath<T> = T extends object
    ? {
        [K in keyof T]: T[K] extends (infer U)[]
        ? [K] | [K, number, ...KeyPath<U>] // Arrays
        : T[K] extends Record<string, infer V>
        ? [K] | [K, string] | [K, string, ...KeyPath<V>] // Records
        : T[K] extends object
        ? [K] | [K, ...KeyPath<T[K]>] // Objects
        : [K]; // Simple fields
    }[keyof T]
    : never;
Enter fullscreen mode Exit fullscreen mode

Next, we determine the value type associated with the specific key path:

export type ValueForKeyPath<T, P extends KeyPath<T>> =
    P extends [infer K, ...infer Rest]
    ? K extends keyof T
        ? Rest extends []
            ? T[K] // Return current value
            : T[K] extends (infer U)[]
            ? Rest extends [number, ...infer R]
                ? ValueForKeyPath<U, R extends KeyPath<U> ? R : never>
                : never
            : T[K] extends Record<string, infer U>
            ? Rest extends [string, ...infer R]
                ? ValueForKeyPath<U, R extends KeyPath<U> ? R : never> | U
                : U
            : T[K] extends object
            ? ValueForKeyPath<T[K], Rest extends KeyPath<T[K]> ? Rest : never>
            : never
        : never
    : never;
Enter fullscreen mode Exit fullscreen mode

If the value assigned doesn’t match the type derived from the key path, a static error is raised, ensuring that only valid data can be assigned to the given key path.

For example:

await auth.updateClientAndServerArray([
    {
        keyPath: ['teams', 'response', 'transformations', id, "name"],
        value: "some-name",
        operation: 'setValue',
    },
]);
Enter fullscreen mode Exit fullscreen mode

This type-safe approach ensures that modifying nested structures is both flexible and reliable, providing a solid foundation for future-proofing your code.

Top comments (0)