1

I'm using Typescript 3.9.x

Supposed I have an interface:

interface mytype {
    foo: Foo
    bar: Bar
    baz: Baz
}

I want to achieve a OnlyOneOfType<T> type that allows only one member within the interface.

Such that:

const test1: OnlyOneOfType<mytype> = {foo: 'FOO'}; // PASSES
const test2: OnlyOneOfType<mytype> = {bar: 'BAR'}; // PASSES

const test3: OnlyOneOfType<mytype> = {foo: 'FOO', bar: 'BAR'}; // fails
Terry
  • 149
  • 1
  • 3
  • 12
  • Your question is not clear to me. Are you looking for a `Set`? – Gabriel Lima Mar 08 '21 at 00:57
  • I can give you a straightforward solution that would handle your sample cases but would also accept objects where the unwanted properties are `undefined`, e.g. `{foo: 'FOO', bar: undefined}`. Rejecting these will be a lot harder. – Oblosys Mar 08 '21 at 01:27
  • @Oblosys I prefer one where the undefined property are rejected but I'd like to see the straightforward solution too. – Terry Mar 08 '21 at 01:38
  • I'll have a look tomorrow, it might just be possible. The `RequireOnlyOne` solution has the same problem as my simple one in that it also accepts `{foo: 'FOO', bar: undefined}`. – Oblosys Mar 08 '21 at 02:06
  • 1
    @Terry, please note that asking for `OnyOneOfType` to be `{foo: Foo} | {bar: Bar} | {baz: Baz}` is not the same as asking for it to allow only one member (see [this q/a](//stackoverflow.com/questions/46370222/typescript-a-b-allows-combination-of-both)). Which one do you want? The former is straightforward; the latter is less so but still reasonable. Both will allow `{foo: "FOO", bar: undefined}`. There is no specific type in TypeScript which will allow `{foo: "FOO"}` and reject `{foo: "FOO", bar: undefined}`. You can use generics to get closer, but I doubt it's worth the effort. – jcalz Mar 08 '21 at 02:12
  • which, btw, the former is `type OnlyOneOfType = { [K in keyof T]: T[K] extends unknown ? { [Key in K]: T[K] } : never; }[keyof T]` – Federkun Mar 08 '21 at 02:14
  • @jcalz Thank you. I've edited my question/post now. I'm seeking the latter. The post you've linked was very useful. – Terry Mar 08 '21 at 02:40
  • @Federkun is there some advantage to that over `{[K in keyof T]: Pick}[keyof T]`? I'm not sure why you are doing a `T[K] extends unknown` check in there, and `Pick` is homomorphic (so `{readonly a?: string}` in will be `{readonly a?: string}` out and not `{a: string | undefined}`) – jcalz Mar 08 '21 at 02:42
  • None, im just bad at typescript – Federkun Mar 08 '21 at 07:28
  • @jcalz Are you certain about that? Both solutions seem to infer `{a?: string | undefined}`, with the `?` and also an extra `| undefined`. They also incorrectly include `undefined` in the union. You can avoid the latter with a conditional definition that remaps the keys: https://tsplay.dev/wQAeAw (requires TypeScript v4.1 though). – Oblosys Mar 08 '21 at 16:10
  • @Terry My idea from last night made no sense, I thought about enumerating all configurations of `never` props, but having the bare singleton objects in the union means objects with multiple keys won't be rejected, so it seems the `undefined`s can't be avoided. – Oblosys Mar 08 '21 at 16:15
  • 1
    @Oblosys heh no I guess you're right. I suppose having `K in keyof T` earlier is enough to maintain homomorphicity (that's not a word) with `P in K` later. I didn't realize that. (Including `undefined` in the union happens even if it's not homomorphic though) – jcalz Mar 08 '21 at 16:23

1 Answers1

1

Do you mean this type?

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 OnlyOneOfType<T> = RequireOnlyOne<T, keyof T>

interface mytype {
    foo: 'FOO'
    bar: 'BAR'
    baz: 'BAZ'
}

const test1: OnlyOneOfType<mytype> = { foo: 'FOO' }; // PASSES
const test2: OnlyOneOfType<mytype> = { bar: 'BAR' }; // PASSES
const test3: OnlyOneOfType<mytype> = { foo: 'FOO', bar: 'BAR' }; // FAILS

That's the same as KPD's answer in this question...url

zixiCat
  • 1,019
  • 1
  • 5
  • 17