Typing Object Properties as Strings in TypeScript
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:
1 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:
1 type PropertiesOnly<T> = Pick<2 T,3 {4 [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;5 }[keyof T]6 >;78 type IsArray<T> = T extends (infer U)[] ? true : false;910 export type ValidPath<T, Prefix extends string = ''> = T extends object11 ? {12 [K in keyof PropertiesOnly<T> & (string | number)]: IsArray<T[K]> extends true13 ? never14 : `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}`;15 }[keyof PropertiesOnly<T> & string]16 : never;
Explanation:
- Exclude Functions:
PropertiesOnly
filters out methods from the object. - Handle Arrays:
IsArray
excludes arrays from the recursion to avoid complex union types. - Recursive Type: The
ValidPath
type 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.
1 export function validatePath<T>(path: ValidPath<T>): ValidPath<T> {2 return path; // All checks occur during compilation3 }
Example Usage
Given this object:
1 const data = {2 user: {3 name: "Alice",4 details: { age: 30 },5 },6 };
You can validate property paths like this:
1 validatePath<typeof data>("user.name"); // Valid2 validatePath<typeof data>("user.details.age"); // Valid3 validatePath<typeof data>("user.invalid"); // Error
Additional Example
For more advanced scenarios:
1 function floatHandler<T>(2 value: string,3 row: any,4 property: ValidPath<T>,5 propertyRaw: string6 ) {7 // Your code here8 }910 floatHandler<typeof data["user"]>(11 "42",12 data.user,13 "details.age",14 "age"15 );
This approach ensures type safety and prevents runtime errors caused by invalid property names.