Last active
March 20, 2022 22:55
-
-
Save LevPewPew/818043c1ccd5201dab225c982f2df719 to your computer and use it in GitHub Desktop.
RequireAtLeastOne and RequireOnlyOne custom utility types
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = | |
| Pick<T, Exclude<keyof T, Keys>> | |
| & { | |
| [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> | |
| }[Keys] | |
| // ************************************************************ | |
| interface MenuItem { | |
| title: string; | |
| component?: number; | |
| click?: number; | |
| icon: string; | |
| } | |
| type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'> | |
| const withComponent: ClickOrComponent = { | |
| title: "test", | |
| component: 52, | |
| icon: "icon" | |
| } | |
| const withClick: ClickOrComponent = { | |
| title: "test", | |
| click: 54, | |
| icon: "icon" | |
| } | |
| const errorWithNeither: ClickOrComponent = { | |
| title: "test", | |
| icon: "icon" | |
| } | |
| const noErrorWithBoth: ClickOrComponent = { | |
| title: "test", | |
| click: 54, | |
| component: 24, | |
| icon: "icon" | |
| } | |
| const errorWithBothWhenOneHasWrongType: ClickOrComponent = { | |
| title: "test", | |
| click: 54, | |
| component: "should be number here", | |
| icon: "icon" | |
| } | |
| // ************************************************************ | |
| interface NothingIsRequired { | |
| foo?: string; | |
| bar?: string; | |
| fizz?: number; | |
| buzz?: number; | |
| } | |
| type MustHaveOneOrMore = RequireAtLeastOne<NothingIsRequired> | |
| const withFoo: MustHaveOneOrMore = {foo: "something"} | |
| const withAll: MustHaveOneOrMore = {foo: "something", bar: "something else", fizz: 123, buzz: 789} | |
| const errorWithoutAny: MustHaveOneOrMore = {} | |
| const errorWithoutAnyCorrect: MustHaveOneOrMore = { wrong: "something", alsoWrong: "something else"} | |
| const errorWithOneCorrectKeyButWrongType: MustHaveOneOrMore = { foo: 123, bar: "something"} | |
| // ************************************************************ | |
| interface NothingIsRequired { | |
| foo?: string; | |
| bar?: string; | |
| fizz?: number; | |
| buzz?: number; | |
| } | |
| type MustHaveOneOrMoreSpecific = RequireAtLeastOne<NothingIsRequired, "foo" | "buzz"> | |
| const withFooAndBuzz: MustHaveOneOrMoreSpecific = {foo: "something", buzz: 123}; | |
| const withOnlyFoo: MustHaveOneOrMoreSpecific = {foo: "something"}; | |
| const errorWithOnlyBar: MustHaveOneOrMoreSpecific = {bar: "asd"}; | |
| const withAll2: MustHaveOneOrMoreSpecific = {foo: "something", bar: "something else", fizz: 123, buzz: 789} | |
| const errorWithoutAny: MustHaveOneOrMoreSpecific = {} | |
| const errorWithoutAnySpecifiedOptionals: MustHaveOneOrMoreSpecific = { fizz: 123, bar: "something else"} | |
| const errorWithOneCorrectKeyButWrongType: MustHaveOneOrMoreSpecific = { foo: 123, buzz: 345} | |
| // ************************************************************ | |
| 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] | |
| // ************************************************************ | |
| type OnlyOneClickOrComponent = RequireOnlyOne<MenuItem, 'click' | 'component'> | |
| const noErrorWithOnlyOne: OnlyOneClickOrComponent = { | |
| title: "test", | |
| click: 534, | |
| icon: "icon" | |
| } | |
| const errorWithBoth: OnlyOneClickOrComponent = { | |
| title: "test", | |
| click: 534, | |
| component: 53, | |
| icon: "icon" | |
| } | |
| // ************************************************************ | |
| // WARNING: | |
| // WARNING: | |
| // WARNING: | |
| // ************************************************************ | |
| // This interface will be used to trick OnlyOneClickAtComponent into allowing an object with both | |
| interface ClickMenuItem { | |
| title: string; | |
| click: number; | |
| icon: string; | |
| } | |
| const hasBoth = { | |
| title: "test", | |
| click: 54, | |
| component: 24, | |
| icon: "icon" | |
| } | |
| // No error because excess properties are only disallowed when directly assigning an object literal | |
| const temp: ClickMenuItem = hasBoth | |
| // No error despite temp actually having both because TS only knows that temp is a ClickMenuItem | |
| const trickIntoAllowingBoth: OnlyOneClickOrComponent = temp |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment