打字稿:有递归keyof吗?

Dan*_*lan 3 types typescript

有没有办法让这样的代码编译并保持类型安全?

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"
// }
Run Code Online (Sandbox Code Playgroud)

您可以在此处使用此代码。

Dav*_*ret 9

这可以通过TypeScript 4.1 中的新模板文字类型和递归类型实现。

属性和索引访问类型

这里有几种定义它的方法,可以超越单一级别。我推荐第一种方法,因为它的公共 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}`;
Run Code Online (Sandbox Code Playgroud)
export type RecursiveKeyOf<TObj extends object, isFirstLevel extends boolean = true> = {
  [TKey in keyof TObj & (string | number)]:
    isFirstLevel extends true
      ? RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>
      : RecursiveKeyOfHandleValue<TObj[TKey], RecursiveKeyOfAccess<TKey>>;
}[keyof TObj & (string | number)];

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

type RecursiveKeyOfAccess<TKey extends string | number> =
  | `['${TKey}']`
  | `.${TKey}`;
Run Code Online (Sandbox Code Playgroud)

仅属性访问类型

如果您只需要访问属性,那就简单多了:

export type RecursiveKeyOf<TObj extends object> = {
  [TKey in keyof TObj & (string | number)]:
    TObj[TKey] extends object
      ? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
      : `${TKey}`;
}[keyof TObj & (string | number)];
Run Code Online (Sandbox Code Playgroud)

解释和分解

export type RecursiveKeyOf<TObj extends object> = (
  (
    // 1. 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
      [TPropName in keyof TObj & (string | number)]:
        HandleProperty<TObj[TPropName], TPropName>;
    }
  )[
    keyof TObj & (string | number) // for every string or number property name
  ] // 2. Now flatten the object's property types to a final union type
);

type HandleProperty<TValue, TPropName extends string | number> =
  // If the value of the property is an object type...
  TValue extends object
    // Then...
      // 1. Return the current property name as a string
    ? `${TPropName}`
      // 2. And return the property name concatenated with a `.` and
      //    all the return values of `RecrusiveKeyOf<TValue>`
      | `${TPropName}.${RecursiveKeyOf<TValue>}`
    // Else, only return the current property name as a string
    : `${TPropName}`;
Run Code Online (Sandbox Code Playgroud)

例如:

// 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"
Run Code Online (Sandbox Code Playgroud)

  • “TS2589:类型实例化过深并且可能是无限的。” - TS 4.6.3 (2认同)

Dan*_*lan 5

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

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"
// }
Run Code Online (Sandbox Code Playgroud)

你可以在这里看到它的工作原理。

这是具有更好文档的类型:

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];
Run Code Online (Sandbox Code Playgroud)