首页 > 解决方案 > 打字稿:是否有递归键?

问题描述

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

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

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

标签: typescripttypes

解决方案


这可以通过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], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];

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

仅限属性访问类型

如果您只需要属性访问,则要简单得多:

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)];

解释和细分

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;

例如:

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

推荐阅读