When working with functions or methods that accept an object and its property as a string, it can be challenging to maintain type safety as objects evolve. Here’s how to create a utility type in TypeScript to ensure that property names are type-checked at compile time.
Problem
Consider this function call:
updateDate(user, 'date');If the property changes (user.date → user.birthday), the compiler won’t catch the mismatch, leading to potential bugs during runtime.
Solution
We can use a custom utility type, ValidPath, to ensure type safety for object properties.
Step 1: Define the Type
The ValidPath type recursively generates string representations of all object keys, including nested properties:
type PropertiesOnly<T> = Pick<
T,
{
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
}[keyof T]
>;
type IsArray<T> = T extends (infer U)[] ? true : false;
export type ValidPath<T, Prefix extends string = ''> = T extends object
? {
[K in keyof PropertiesOnly<T> & (string | number)]: IsArray<
T[K]
> extends true
? never
: `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}`;
}[keyof PropertiesOnly<T> & string]
: never;Explanation:
- Exclude Functions:
PropertiesOnlyfilters out methods from the object. - Handle Arrays:
IsArrayexcludes arrays from the recursion to avoid complex union types. - Recursive Type: The
ValidPathtype generates all possible key paths in the object, including nested properties.
Step 2: Create a Utility Function
The validatePath function ensures that only valid property paths are accepted.
export function validatePath<T>(path: ValidPath<T>): ValidPath<T> {
return path; // All checks occur during compilation
}Example Usage
Given this object:
const data = {
user: {
name: 'Alice',
details: { age: 30 },
},
};You can validate property paths like this:
validatePath<typeof data>('user.name'); // Valid
validatePath<typeof data>('user.details.age'); // Valid
validatePath<typeof data>('user.invalid'); // ErrorAdditional Example
For more advanced scenarios:
function floatHandler<T>(
value: string,
row: any,
property: ValidPath<T>,
propertyRaw: string
) {
// Your code here
}
floatHandler<(typeof data)['user']>('42', data.user, 'details.age', 'age');This approach ensures type safety and prevents runtime errors caused by invalid property names.