Last active
March 14, 2025 21:11
-
-
Save nickhudkins/0ede21f00d92f88736de4457e0a279f6 to your computer and use it in GitHub Desktop.
TypeSafe Path Proxy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Make all properties in T deeply non-nullable recursively | |
type DeepNonNullable<T> = T extends object | |
? { [K in keyof T]-?: DeepNonNullable<NonNullable<T[K]>> } | |
: NonNullable<T>; | |
/** | |
* Creates a proxy that builds a dotted path string based on property access | |
* while restricting property access to only those defined in the generic type T. | |
* | |
* @param path Current accumulated path | |
* @returns A proxy object that tracks property access as a dotted path | |
*/ | |
/** | |
* Creates a proxy that builds a dotted path string based on property access | |
* while restricting property access to only those defined in the generic type T. | |
* All nullable properties are treated as non-nullable to ensure safe path generation. | |
* | |
* @param path Current accumulated path | |
* @returns A proxy object that tracks property access as a dotted path but appears as type T | |
*/ | |
export function createPathProxy<T extends object>(path: string = ''): DeepNonNullable<T> { | |
// List of properties to exclude from path generation | |
const excludedProps = new Set([ | |
'call', | |
'apply', | |
'bind', | |
'name', | |
'length', | |
'arguments', | |
'caller', | |
'constructor', | |
'prototype', | |
'__proto__', | |
'__defineGetter__', | |
'__defineSetter__', | |
'__lookupGetter__', | |
'__lookupSetter__' | |
]); | |
const handler: ProxyHandler<any> = { | |
get(_, prop) { | |
// Handle special cases | |
if (typeof prop === 'symbol') { | |
if (prop === Symbol.toPrimitive) { | |
return () => path; | |
} | |
return undefined; | |
} | |
// Handle toString specially | |
if (prop === 'toString') { | |
return () => path; | |
} | |
// Skip excluded properties | |
if (excludedProps.has(prop as string)) { | |
return undefined; | |
} | |
// Handle numeric indexing for arrays | |
if (typeof prop === 'string' && !isNaN(Number(prop))) { | |
const index = Number(prop); | |
const newPath = `${path}[${index}]`; | |
return createPathProxy<any>(newPath); | |
} | |
// Build the new path by appending the property | |
const propStr = String(prop); | |
const newPath = path ? `${path}.${propStr}` : propStr; | |
// Return a new proxy with the updated path | |
return createPathProxy<any>(newPath); | |
}, | |
apply() { | |
// When called as a function, return the path | |
return path; | |
} | |
}; | |
// Use a function as the target so it can be both called and have properties accessed | |
const proxyTarget = function () { return path; }; | |
return new Proxy(proxyTarget, handler) as unknown as DeepNonNullable<T>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment