Future-proof code means that it is designed and written in such a way that it remains maintainable in the long term and can be adapted to new requirements with minimal changes.
Let's assume we have three business entities in TypeScript: a user, the teams associated with the user, and, for each team, a set of transformations. Each transformation has a name and texts.
export interface UserDTO {
teams: { [teamId: string]: TeamDTO };
}
export interface TeamDTO {
transformations: { [transformationId: string]: Transformation };
}
export interface Transformation {
name: string;
texts: { [id: string]: string };
}
To modify these entities, we follow the command pattern. The modification parameters are wrapped in an object: the value
, and the keyPath
which indicates where the modification should occur, the type of modification operation is represented by the setValue
keyword.
await auth.updateClientAndServerArray([
{
keyPath: ['teams', 'response', 'transformations', id, 'name'],
value: 'some-name',
operation: 'setValue',
},
]);
The advantage of this approach is that a single abstract manipulation method with conditional types can handle all future modifications of the business entities on both the client-side and server-side. On the backend, if you're using a document-based database with a similar enclosing structure for business entities, the modification can also be implemented using a single endpoint.
This approach ensures strong type safety, but it requires some conditional types.
We perform two operations: the first duplicates a transformation and sets a new name for the newly created transformation. The second example modifies only the transformation's name.
Type safety is ensured in two ways:
- If the
keyPath
points to an invalid location, a type error is raised. - If the value doesn’t match the type expected at the
keyPath
, a type error occurs.
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>
Here, the implementation of the updateClientAndServerArray
method itself is not shown, but it uses a recursive approach. The same method handles both duplication and name setting.
To update the name field, updateClientAndServerArray
can be used like this:
await auth.updateClientAndServerArray([
{
keyPath: ['teams', 'response', 'transformations', id, 'name'],
value: 'some-name',
operation: 'setValue',
},
]);
The key advantage here is that if a new field or container is added to any business entity in the future, no new manipulation methods are needed. The modification can be carried out without requiring changes to the backend or the client, and type safety is preserved, ensuring that no invalid values can be assigned. This approach can handle any JSON type, including strings, numbers, objects, or arrays.
Type Definitions
These two types are used to ensure type safety. The first type 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>] // Objects
: T[K] extends object
? [K] | [K, ...KeyPath<T[K]>]
: [K]; // Simple fields
}[keyof T]
: never;
Next, we determine the value type associated with a 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;
If the value being 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',
},
]);
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)