typescript:有递归键吗?

typescript:有递归键吗?,typescript,types,Typescript,Types,有没有办法让这样的代码编译并成为类型安全的 type ComplexObject = { primitive1: boolean; complex: { primitive2: string; primitive3: boolean; } }; interface MyReference { myKey: keyof ComplexObject; } const works1: MyReference = { myKey: "primitive1

有没有办法让这样的代码编译并成为类型安全的

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"
// }

您可以随意使用此代码。

不,不幸的是,Typescript无法做到这一点

编辑:,有关如何在递归类型中使用它们,请参阅

它支持的唯一接近的东西是路径的递归数组:

type Cons<H, T> = T extends readonly any[] ?
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : 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 Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
        P extends [] ? never : Cons<K, P> : never
    ) }[keyof T]
    : [];

type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

interface MyReference {
  myKey: Paths<ComplexObject>;
}

const works1: MyReference = {
  myKey: ["primitive1"]
}

const works2: MyReference = {
  myKey: ["complex"]
}

const iWantThisToCompile1: MyReference = {
  myKey: ["complex", "primitive2"]
}

const iWantThisToCompile2: MyReference = {
  myKey: ["complex", "primitive3"]
}
type Cons=T扩展只读任何[]?
((h:h,…t:t)=>void)扩展((…r:inferr)=>void)?R:从来没有
:从不;
类型Prev=[从不,0,1,2,3,4,5,6,7,8,9,10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
类型路径=[D]扩展[从不]?从不:T扩展对象?
{[K in keyof T]-?:[K]|(路径扩展推断P?
P扩展[]?从不:缺点:从不
)}[keyof T]
: [];
类型ComplexObject={
原语1:布尔;
复杂:{
原语2:字符串;
原语3:布尔;
}
};
接口MyReference{
myKey:路径;
}
常量工作1:MyReference={
myKey:[“原语1”]
}
常量工作2:MyReference={
myKey:[“复杂”]
}
const iWantThisToCompile1:MyReference={
myKey:[“复杂”、“基本2”]
}
const iWantThisToCompile2:MyReference={
myKey:[“复杂”、“原始3”]
}
像lodash的
get
这样的库既可以使用
“complex.primitive2”
也可以使用
[“complex”,“primitive2”]
这样的路径数组。因此,虽然这可能不是您想要的确切答案,但希望它能为您提供更安全的类型选择


我不认为这是完全重复的,但下面是我从中获得的
路径
类型别名的答案:

这在中使用新的模板文本类型和递归类型是可能的

属性和索引访问类型 这里有两种定义方法,它们可以超越单个级别。我推荐第一种方法,因为它的公共API中没有其他未使用的类型参数

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], RecursiveKeyOfAccess<TKey>>;
}[keyof TObj & (string | number)];

type RecursiveKeyOfHandleValue<TValue, Text extends string> =
  TValue extends object
    ? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
    : Text;

type RecursiveKeyOfAccess<TKey extends string | number> =
  | `['${TKey}']`
  | `.${TKey}`;

对于模板文本,这应该是可行的:

type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

type PathOf<T> =  {
  [K in keyof T]: T[K] extends object ? K | `${K}.${PathOf<T[K]>}` | `${K}['${PathOf<T[K]>}']` : K
}[keyof T]

type PathOfComplexObject = PathOf<ComplexObject>
类型ComplexObject={
原语1:布尔;
复杂:{
原语2:字符串;
原语3:布尔;
}
};
类型pathf={
[K in keyof T]:T[K]扩展对象?K$`{K}.${PathOf}```{K}['${PathOf}']`:K
}[keyof T]
类型PathOfComplexObject=PathOf

操场上显示了一些投诉,但如果您将鼠标悬停在
PathOfComplexObject
上,您可以看到生成的类型。我理解它为什么抱怨:

类型实例化太深,可能是无限的

但我不确定:

类型“K”不能分配给类型“string | number | bigint | boolean | null | undefined”


我在其他地方得到了帮助,得到了这种类型的帮助:

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"
// }
类型ComplexObject={
原语1:布尔;
复杂:{
原语2:字符串;
原语3:布尔;
}
};
RecKeyof类型=
T扩展字符串|数字| bigint |布尔值
|null |未定义|((…args:any)=>any)?从不:{
[K in keyof T&string]:[Prefix]扩展了[never]
?K | `['${K}']` | RecKeyof
:`${Prefix}.${K}` |`${Prefix}['${K}]``RecKeyof
}[T&string键];
接口MyReference{
myKey:RecKeyof;
}
常量工作1:MyReference={
myKey:“原语1”
}
常量工作2:MyReference={
我的钥匙:“复杂”
}
const iWantThisToCompile1:MyReference={
myKey:“复杂。原始2”
}
const iWantThisToCompile2:MyReference={
myKey:“复杂['primitive3']”
}
//常量IDontwanttHistoricCompile1:MyReference={
//myKey:“原语2”
// }
//常量IDontwanttHistoricCompile2:MyReference={
//myKey:“原始3”
// }

以下是具有更好文档的类型:

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];
RecKeyof类型=
//如果T与下面联合中的任何类型匹配,我们就不关心它的属性。
//我们必须排除函数,否则我们会得到无限递归,因为函数
//作为函数的属性:即myFunc.call.call.call.call.call.call.call.call.call。。。
T扩展字符串| number | bigint | boolean | null | undefined |((…args:any)=>any)
? 如果匹配,则不要//跳过T
//如果T不匹配,我们关心它的属性。我们使用映射类型来重写
//T。
//如果T={foo:{bar:string}},则此映射类型生成
//{foo:“foo”|“foo.bar”}
: {
//对于T上的每个属性,我们用
[K输入T和字符串键]:
//当前prefix.key或prefix.key的子项。
`${Prefix}${K}`| RecKeyof
//一旦我们映射了T,我们只关心它的属性值
//所以我们告诉typescript生成映射类型键的并集。
//{foo:“1”,bar:“2”}[“foo”|“bar”]生成“1”|“2”
}[T&string键];

太酷了,我认为这适合我的需要。有没有一种方式可以说路径仅限于布尔类型的属性?签出。
路径
类型大量使用它们,我确信有一种方法可以查看值的类型是否扩展了
布尔值
,或者如果不是,则使用
从不
。我只是试着自己修改它,但没能弄明白…哦,这很酷,我以前试过在类型中进行字符串连接,但我一定是做错了。啊,那是在4.1之前,你能解释一下这是怎么回事吗?理想情况下,解释属性访问支持一个,因为它是simpler@DanielKaplan我为属性acess-only类型添加了一个解释。属性和索引访问类型遵循类似的原则。当其中一个属性是数组(例如name:string[])时,此解决方案也会递归到数组的方法中,这是不可取的。也就是说,RecursiveKeyOf类型包括names.concat、names.values等。如何防止这种情况发生?
pathf
如果要保留括号符号,还需要更多的爱,因为向
添加另一层。complex
可能会导致类型
复杂['primitive2.foo']
和其他。显然,将
K
string
相交可以解析
而不是a
// 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"
type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

type PathOf<T> =  {
  [K in keyof T]: T[K] extends object ? K | `${K}.${PathOf<T[K]>}` | `${K}['${PathOf<T[K]>}']` : K
}[keyof T]

type PathOfComplexObject = PathOf<ComplexObject>
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"
// }
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];