genéricos - ¿Existe una manera de declarar un tipo unión en TypeScript con cadenas computadas basadas en constantes existentes?
Supongamos que tenemos la siguiente constante:
const something = {
foo: {
bar: {
num: 67,
str: 'str',
},
},
some: {
prop: 12,
},
topProp: 25,
};
La tarea:
Implementar verificación de tipos para la siguiente función de acceso a propiedades profundas
/**
* @example
* getByPath('foo/bar/str'); // returns 'str'
*
* @example
* getByPath('topProp'); // returns 25
*
* @example
* getByPath('some/prop'); // returns 12
*/
const getByPath = (path: ComputedUnionType) => {<unrelated-code-magic>};
// Where
type ComputedUnionType = 'foo/bar/num' | 'foo/bar/str' | 'some/prop' | 'topProp';
// or even better
type ComputedUnionType<typeof some> = 'foo/bar/num' | 'foo/bar/str' | 'some/prop' | 'topProp';
const getByPath = <T>(path: ComputedUnionType<T>) => ...
¿Qué hice?
- Se implementó una función para obtener un arreglo de rutas válidas, pero devuelve un arreglo de cadenas simples (obviamente -_-), así que no pude encontrar ninguna manera de usarlo para hacer cumplir los tipos
- Leí un montón de artículos sobre tipos enum, resultando en la conclusión de que los tipos enum no me ayudarán aquí, porque sus valores de propiedad solo pueden ser números computados, no cadenas (y probablemente no ayudarán de todos modos, porque sus propiedades en sí no pueden generarse sobre la marcha)
- Me encontré con esta respuesta sobre la implementación de la verificación de tipos de tuplas, pero de alguna manera no logré hacer uso de ella en mi caso. Es una lectura bastante interesante, pero en general la solución proporcionada juega con los tipos de unión existentes y las claves, pero nunca calcula tipos nuevos.
Suposiciones
- Tal vez podría ser un tipo que se llama a sí mismo recursivamente, como deep partial o algo así
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
- Quizás haya alguna manera de lograrlo desde abajo con tipos genéricos, tomando keyof bar, keyof foo y así sucesivamente.
Respuesta #1
Cuando TypeScript 4.1 llegue, podrás manipular tipos de literales de cadena a través de tipos de literales de plantilla como se implementa en microsoft/TypeScript#40336. Aquí hay una posible implementación para convertir un tipo en una unión de rutas delimitadas por barra que conducen a propiedades que no son objetos:
type Join<K, P> = K extends string | number ?
P extends string | number ?
`${K}${"" extends P ? "" : "/"}${P}`
: never : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T] : "";
He puesto limitadores de recursión explícitos aquí, así que si intentas hacer Leaves<SomeTreelikeType> puedes elegir una profundidad máxima. Si no te importa, puedes olvidar Prev y D y simplemente tener
type Leaves<T> = T extends object ?
{ [K in keyof T]-?: Join<K, Leaves<T[K]>> }[keyof T] : "";
Esto te da la unión que deseas:
type ComputedUnionType = Leaves<typeof something>;
// type ComputedUnionType = "topProp" | "foo/bar/str" | "foo/bar/num" | "some/prop"
La parte que no preguntaste es cómo hacer que el compilador convierta el tipo de la ruta al tipo del resultado de la salida. Esto también es posible (con tipos condicionales recursivos como se implementó en #40002, que también llegará en TS4.1), pero dado que no preguntaste, no voy a invertir el tiempo en implementarlo.
Enlace de Playground al código
Respuesta #2
También podría agregar mi solución, que es mayormente equivalente a la versión de recursión sin verificación de jcalz, pero con el ayudante Join combinado. En el ejemplo se comporta igual, pero puede comportarse de manera diferente si tu objeto tiene valores que no sean cadenas o números, o si el tipo de tu objeto tiene claves opcionales ?.
type ComputedUnionType<T, S extends string = '/'> = {
[K in keyof T]:
K extends string
? ComputedUnionType<T[K]> extends string
? `${K}${S}${ComputedUnionType<T[K]>}`
: K
: never;
}[keyof T];
type Z = ComputedUnionType<typeof something>;
// type Z = "topProp" | "foo/bar/str" | "foo/bar/num" | "some/prop"
Enlace al parque
