tipos - typescript: ¿hay un keyof recursivo?
¿Hay alguna forma de que un código como este se compile y sea seguro en tipos?
type ComplexObject = {
primitive1: boolean;
complex: {
primitive2: string;
primitive3: boolean;
}
};
interface MyReference {
myKey: keyof ComplexObject;
}
const works1: MyReference = {
myKey: "primitive1"
}
const works2: MyReference = {
myKey: "complex"
}
const iWantThisToCompile1: MyReference = {
myKey: "complex.primitive2" // Error: Type '"complex.primitive2"' is not assignable to type '"primitive1" | "complex"'.
}
const iWantThisToCompile2: MyReference = {
myKey: "complex['primitive3']" // Error: Type '"complex['primitive3']"' is not assignable to type '"primitive1" | "complex"'.
}
// const iDontWantThisToCompile1: MyReference = {
// myKey: "primitive2"
// }
// const iDontWantThisToCompile2: MyReference = {
// myKey: "primitive3"
// }
Puedes experimentar con este código aquí.
Respuesta #1
Esto es posible con los nuevos tipos literales de plantilla y tipos recursivos en TypeScript 4.1.
Tipo de acceso a propiedades e índices
Aquí hay una forma de definir esto que funciona más allá de un solo nivel. Es posible usar menos tipos que estos, pero este enfoque no tiene parámetros de tipo adicionales sin usar en su API pública.
export type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
}[keyof TObj & (string | number)];
type RecursiveKeyOfInner<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];
type RecursiveKeyOfHandleValue<TValue, Text extends string> =
TValue extends any[] ? Text :
TValue extends object
? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
: Text;
Tipo de solo acceso a la propiedad
Si solo necesitas acceso a la propiedad, es mucho más simple:
export type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
TObj[TKey] extends any[] ? `${TKey}` :
TObj[TKey] extends object
? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
: `${TKey}`;
}[keyof TObj & (string | number)];
Explicación y Desglose
export type RecursiveKeyOf<TObj extends object> = (
(
// Create an object type from `TObj`, where all the individual
// properties are mapped to a string type if the value is not an object
// or union of string types containing the current and descendant
// possibilities when it's an object type.
{
// Does this for every property in `TObj` that is a string or number
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
}
)[
keyof TObj & (string | number) // for every string or number property name
] // Now flatten the object's property types to a final union type
);
// This type does the same as `RecursiveKeyOf`, but since
// we're handling nested properties at this point, it creates
// the strings for property access and index access
type RecursiveKeyOfInner<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];
type RecursiveKeyOfHandleValue<TValue, Text extends string> =
// If the value is an array then ignore it, providing back
// only the passed in text
TValue extends any[] ? Text :
// If the value is an object...
TValue extends object
// Then...
// 1. Return the current property name as a string
? Text
// 2. Return any nested property text concatenated to this text
| `${Text}${RecursiveKeyOfInner<TValue>}`
// Else, only return the current text as a string
: Text;
Por ejemplo:
// this type
{
prop: { a: string; b: number; };
other: string;
}
// goes to
{
prop: "prop" | "prop.a" | "prop.b";
other: "other";
}
// goes to
"prop" | "prop.a" | "prop.b" | "other"
Respuesta #2
Recibí ayuda en otro lugar y me dieron este tipo:
type ComplexObject = {
primitive1: boolean;
complex: {
primitive2: string;
primitive3: boolean;
}
};
type RecKeyof<T, Prefix extends string = never> =
T extends string | number | bigint | boolean
| null | undefined | ((...args: any) => any ) ? never : {
[K in keyof T & string]: [Prefix] extends [never]
? K | `['${K}']` | RecKeyof<T[K], K>
: `${Prefix}.${K}` | `${Prefix}['${K}']` | RecKeyof<T[K],`${Prefix}.${K}` | `${Prefix}['${K}']`>
}[keyof T & string];
interface MyReference {
myKey: RecKeyof<ComplexObject>;
}
const works1: MyReference = {
myKey: "primitive1"
}
const works2: MyReference = {
myKey: "complex"
}
const iWantThisToCompile1: MyReference = {
myKey: "complex.primitive2"
}
const iWantThisToCompile2: MyReference = {
myKey: "complex['primitive3']"
}
// const iDontWantThisToCompile1: MyReference = {
// myKey: "primitive2"
// }
// const iDontWantThisToCompile2: MyReference = {
// myKey: "primitive3"
// }
Puedes verlo funcionando aquí.
Aquí está el tipo con mejor documentación:
type RecKeyof<T, Prefix extends string = ""> =
// If T matches any of the types in the union below, we don't care about its properties.
// We must exclude functions, otherwise we get infinite recursion 'cause functions have
// properties that are functions: i.e. myFunc.call.call.call.call.call.call...
T extends string | number | bigint | boolean | null | undefined | ((...args: any) => any )
? never // skip T if it matches
// If T doesn't match, we care about it's properties. We use a mapped type to rewrite
// T.
// If T = { foo: { bar: string } }, then this mapped type produces
// { foo: "foo" | "foo.bar" }
: {
// For each property on T, we remap the value with
[K in keyof T & string]:
// either the current prefix.key or a child of prefix.key.
`${Prefix}${K}` | RecKeyof<T[K],`${Prefix}${K}.`>
// Once we've mapped T, we only care about the values of its properties
// so we tell typescript to produce the union of the mapped types keys.
// { foo: "1", bar: "2" }["foo" | "bar"] generates "1" | "2"
}[keyof T & string];
