首页 > 解决方案 > 如何在 TypeScript 中创建 RequireOnlyOne 嵌套类型?

问题描述

我经常看到这个代码片段:

type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
    [K in Keys]-?:
        Required<Pick<T, K>>
        & Partial<Record<Exclude<Keys, K>, undefined>>
}[Keys]

在这里你可以找到我从哪里提出的问题。

它有效,但是,我有这个结构:

export interface MenuItems {
  firstLevel: {
    secondLevel: ['one', 'two']
  };
  anotherFirstLevel: {
    anotherSecondLevel: ['one', 'two'],
    oneMoreSecondLevel: null
  };
}

我需要申请RequireOnlyOne第一级和第二级,但我不知道要在类型上更改什么,RequireOnlyOne因此它适用于每个 firstLevel 键,但也适用于 secondLevel 键。就像现在一样,我只能选择一个 firstLevel,但可以选择该 firstLevel 的多个 secondLevel。

我还尝试用一个对象组成一个新类型,该对象可能是键RequireOnlyOne<keyof MenuItems>和一个也RequireOnlyOne用于值的值,但无法实现。

我想要的示例,将所需的类型称为customType

const workingObject: customType = {
  firstLevel: { // Just one property of the first level
    secondLevel: ['one', 'two'] // Just one property of the second level
  };
}

const errorObject: customType = {
  firstLevel: {
    secondLevel: ['one', 'two']
  };
  anotherFirstLevel: { // Should not work as I am including 2 properties for the first level
    anotherSecondLevel: ['one', 'two']
  };
}

const anotherErrorObject: customType = {
  anotherFirstLevel: {
    anotherSecondLevel: ['one', 'two'],
    oneMoreSecondLevel: null // Should not work neither as I am including 2 properties for second level
  };
}

如果对象具有多个一级属性和/或多个二级属性,则该类型应引发错误。使用建议的RequireOnlyOne类型,我可以实现这一点,但仅适用于第一级,但我需要第一级第二级的相同效果。

有任何想法吗?

标签: javascripttypescript

解决方案


我不知道如何更改RequireOnlyOne类型,但我知道如何创建新类型。

export interface MenuItems {
  firstLevel: {
    secondLevel: ['one', 'two']
  };
  anotherFirstLevel: {
    anotherSecondLevel: ['one', 'two']
    oneMoreSecondLevel: null
  };
}

type Primitives = string | number | boolean | null | undefined | bigint | symbol

type UnionKeys<T> = T extends T ? keyof T : never;
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
  T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type Transform<Obj, Keys extends keyof Obj = keyof Obj, Result = never> =
  StrictUnion<
    Keys extends string ? { // #1
      [Key in Keys]:
      Key extends keyof Obj
      ? (Obj[Key] extends Primitives
        ? Obj[Key]
        : (Obj[Key] extends any[]
          ? Obj[Key]
          : Transform<Obj[Key], keyof Obj[Key], Obj[Key]>)
      )
      : never
    } : Result>


type CustomType = Transform<MenuItems>

const workingObject: CustomType = {
  firstLevel: { // Just one property of the first level
    secondLevel: ['one', 'two'] // Just one property of the second level
  },
}


const errorObject: CustomType = {
  firstLevel: {
    secondLevel: ['one', 'two']
  },
  anotherFirstLevel: { // Should not work as I am including 2 properties for the first level
    anotherSecondLevel: ['one', 'two']
  },
}

const anotherErrorObject: CustomType= {
  anotherFirstLevel: {
    anotherSecondLevel: ['one', 'two'],
    oneMoreSecondLevel: null // Should not work neither as I am including 2 properties for second level
  },
}

Transform- 是主要的实用程序类型。递归迭代键。1# Keys extends string- 此行确保Keys is distributet. 这意味着该行之后的整个代码将应用于每个键。请参阅文档以获取更多信息。

我还添加了Obj[Key] extends any[]- 因为您不希望(我想)遍历数组键。

操场


推荐阅读