Sugerencias
← TIL
~4 min de lectura
#typescript#dx#advanced-types

TypeScript puede hacer esto... y seguro no lo estás usando

Si estás escribiendo código TypeScript pero todavía usás string para referenciar claves en objetos anidados (como archivos de traducción o configuraciones), estás perdiendo la mitad de la potencia del compilador.

Dominar los Recursive Template Literal Types te permite crear accesos estrictos que el IDE entiende perfectamente, eliminando los errores silenciosos en tiempo de ejecución. No se trata de sobre-tipar; se trata de diseñar una experiencia de desarrollo (DX) donde el editor no te deje equivocarte.

El bug silencioso#

Antes de ver la solución, veamos el problema. Usar string para acceder a propiedades profundas esconde bugs que TypeScript debería atrapar fácilmente:

Terminal
// Típico en i18n o configs
function getConfig(path: string) {
  /* ... */
}

// Compila perfecto, falla en producción
const userUrl = getConfig("api.enpoints.users");
// ¿El error? Escribiste "enpoints" en vez de "endpoints".
// TS no te avisa, y tu app devuelve undefined en runtime.

El patrón: DeepPath recursivo#

El secreto está en combinar recursividad con Template Literal Types. Mirá cómo podemos extraer todas las rutas posibles de un objeto como si fueran un string unido por puntos:

src/utils/types.ts
type type DeepPath<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? `${K}` | `${K}.${DeepPath<T[K]>}` : `${K}`; }[keyof T & string] : neverDeepPath<function (type parameter) T in type DeepPath<T>T> = function (type parameter) T in type DeepPath<T>T extends object
  ? {
      [function (type parameter) KK in keyof function (type parameter) T in type DeepPath<T>T & string]: function (type parameter) T in type DeepPath<T>T[function (type parameter) KK] extends object
        ? `${function (type parameter) KK}` | `${function (type parameter) KK}.${type DeepPath<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? `${K}` | `${K}.${DeepPath<T[K]>}` : `${K}`; }[keyof T & string] : neverDeepPath<function (type parameter) T in type DeepPath<T>T[function (type parameter) KK]>}`
        : `${function (type parameter) KK}`;
    }[keyof function (type parameter) T in type DeepPath<T>T & string]
  : never;

// Ejemplo de uso:
interface AppConfig {
  
AppConfig.api: {
    endpoints: {
        users: string;
        posts: string;
    };
    timeout: number;
}
api
: {
endpoints: {
    users: string;
    posts: string;
}
endpoints
: {
users: stringusers: string; posts: stringposts: string; }; timeout: numbertimeout: number; }; AppConfig.theme: "dark" | "light"theme: "dark" | "light"; } type
type ConfigPaths = "api" | "theme" | "api.endpoints" | "api.timeout" | "api.endpoints.users" | "api.endpoints.posts"
ConfigPaths
= type DeepPath<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? `${K}` | `${K}.${DeepPath<T[K]>}` : `${K}`; }[keyof T & string] : neverDeepPath<AppConfig>;

Evolución del desarrollador#

A menudo veo que se anotan las configuraciones manualmente (const config: AppConfig = ...). Esto “aplana” el tipo y borra la inferencia específica de tus datos reales.

¿Por qué esto importa? Porque para que DeepPath<T> funcione, necesita conocer la estructura literal exacta. Ahí es donde entra el combo “Wizard”: usamos satisfies. Esto valida que cumplas con la interfaz, pero mantiene los literales intactos para que DeepPath pueda explotar esa precisión al máximo.


const config: AppConfig = {
api: {
  endpoints: {
    users: "/users",
    posts: "/posts"
  },
  timeout: 5000
},
theme: "dark"
};

// Problema: 'config.theme' es solo string
// no "dark" | "light" específicamente aquí.

const config = {
api: {
endpoints: {
users: "/users",
posts: "/posts"
},
timeout: 5000
},
theme: "dark"
} satisfies AppConfig;

// 'config.theme' se mantiene como "dark"
// Y podemos validar cualquier ruta:
function getProp(path: DeepPath<AppConfig>) {
// lógica...
}

💥 Bonus Track: Inferencia de Retorno (DeepValue)#

Validar el string de entrada es genial, pero el verdadero nivel 500 es inferir el tipo de retorno exacto basándose en ese string. Si pido "api.timeout", TypeScript debería saber que me devuelve un number.

Combinamos DeepPath con otro tipo recursivo llamado DeepValue:

src/utils/types.ts
type type DeepValue<T, P> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? DeepValue<T[K], Rest> : never : P extends keyof T ? T[P] : neverDeepValue<function (type parameter) T in type DeepValue<T, P>T, function (type parameter) P in type DeepValue<T, P>P> = function (type parameter) P in type DeepValue<T, P>P extends `${infer function (type parameter) KK}.${infer function (type parameter) RestRest}`
  ? function (type parameter) KK extends keyof function (type parameter) T in type DeepValue<T, P>T
    ? type DeepValue<T, P> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? DeepValue<T[K], Rest> : never : P extends keyof T ? T[P] : neverDeepValue<function (type parameter) T in type DeepValue<T, P>T[function (type parameter) KK], function (type parameter) RestRest>
    : never
  : function (type parameter) P in type DeepValue<T, P>P extends keyof function (type parameter) T in type DeepValue<T, P>T
    ? function (type parameter) T in type DeepValue<T, P>T[function (type parameter) P in type DeepValue<T, P>P]
    : never;

// Magia pura:
function function getConfig<P extends DeepPath<AppConfig>>(path: P): DeepValue<AppConfig, P>getConfig<function (type parameter) P in getConfig<P extends DeepPath<AppConfig>>(path: P): DeepValue<AppConfig, P>P extends type DeepPath<T> = T extends object ? { [K in keyof T & string]: T[K] extends object ? `${K}` | `${K}.${DeepPath<T[K]>}` : `${K}`; }[keyof T & string] : neverDeepPath<AppConfig>>(path: P extends DeepPath<AppConfig>path: function (type parameter) P in getConfig<P extends DeepPath<AppConfig>>(path: P): DeepValue<AppConfig, P>P): type DeepValue<T, P> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? DeepValue<T[K], Rest> : never : P extends keyof T ? T[P] : neverDeepValue<AppConfig, function (type parameter) P in getConfig<P extends DeepPath<AppConfig>>(path: P): DeepValue<AppConfig, P>P> {
// implementación (ej: usando lodash/get)
return {} as any;
}

const 
const url: string
url
= function getConfig<"api.endpoints.users">(path: "api.endpoints.users"): stringgetConfig("api.endpoints.users");
const
const theme: "dark" | "light"
theme
= function getConfig<"theme">(path: "theme"): "dark" | "light"getConfig("theme");

El costo de la magia (Trade-offs)#

Ningún patrón avanzado viene gratis. En producción, abusar de esta recursividad tiene límites técnicos reales que te van a golpear si el objeto crece demasiado.

Template Literal Paths

Pros

  • Cero errores de tipografía en runtime.
  • Refactoring 100% seguro y trazable.
  • Autocompletado nativo sin plugins extra.

Cons

  • Performance del compilador: genera uniones inmensas que ralentizan tu IDE si el objeto base es masivo.
  • Límites de recursión: TypeScript frena (Type instantiation is excessively deep) si pasás de cierta profundidad.
  • Arrays dinámicos: la inferencia se rompe o se complica si intentás navegar índices de arreglos (T[]).

Esta técnica reduce drásticamente los dolores de cabeza. Implementando estos tipos, obligás al compilador a validar la estructura real de tus datos antes del build. Si trabajás en proyectos medianos con configuraciones predecibles o sistemas de i18n, este patrón te ahorra horas de debugging.

Si te interesa llevar la seguridad de tipos al siguiente nivel, pegale una mirada a cómo usamos Algebraic Data Types para pagos indestructibles o cómo migramos a Zod 4 para validación en el Edge.

Enlace copiado al portapapeles