简体   繁体   中英

How to create a RequireOnlyOne nested type in TypeScript?

I've seen this code snippet a lot:

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]

Here you can find the question from where I took it.

It works, however, I have this structure:

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

I need to apply RequireOnlyOne for the first and the second levels, but I can't figure what to change on the type RequireOnlyOne so it works for each firstLevel keys but also with secondLevel keys. As it is right now I can select only one firstLevel but multiple secondLevels of that firstLevel.

I also tried to compose a new type with an object which key could be RequireOnlyOne<keyof MenuItems> and a value that uses also RequireOnlyOne for the values, but couldn't make it.

Example of what I want, calling the desired type as 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
  };
}

The type should throw an error if the object has more than one first level property, and/or more than one second level property. With the proposed RequireOnlyOne type I can achieve that but just for the first level, but I need the same effect for first and second level.

Any ideas?

IMO it's a quite complex question and I'd recommend looking for alternatives and refactor the code.

Otherwise, you might find this helpful:

type RequireOnlyOneUnion<T, Keys extends KeysOfUnion<T> = KeysOfUnion<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;
  };
}

type KeysOfUnion<T> = T extends T ? keyof T : never;

// ok
const a: RequireOnlyOneUnion<MenuItems[keyof typeof x], KeysOfUnion<MenuItems[keyof typeof x]>> = {
  anotherSecondLevel: ["one", "two"],
};
// error
const b: RequireOnlyOneUnion<MenuItems[keyof typeof x], KeysOfUnion<MenuItems[keyof typeof x]>> = {
  secondLevel: ["one", "two"],
  anotherSecondLevel: ["one", "two"],
};

I don't know how to change RequireOnlyOne type but I know how to create new type.

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 - is a main utility type. Recursively iterates over keys. 1# Keys extends string - this line makes sure that that Keys is distributet . It means that whole code which goes after this line will be applied to each key. Please see docs for more info.

I have also added Obj[Key] extends any[] - because you don't want (I suppose) to iterate though arrays keys.

Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM