0

I have a type Identifiers which hold different props and will be used as React component property.

type Identifiers = {
  alias: string;
  personalId: number;
  customerId: number;
};

I want to create a type which will allow to specify only one of identifiers and omit others (not make others optional).

The idea is that specifying any prop from identifiers if one of props already passed to react component should give an error and there should be no suggested other props (if possible). Is there are elegant way to to create type for that?

P.S. I hope this is not a duplicate, but I was unable to find exactly what I want as other solutions require union from many types where other props set to never instead of omitting them or just made optional.

Andyally
  • 863
  • 1
  • 9
  • 22
  • Does this answer your question? [typescript interface require one of two properties to exist](https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist) – Yehor Androsov Dec 08 '22 at 21:17
  • @user7313094, accepted answer requires splitting interfaces which I want to avoid. Other answer work partially as I get error if more that one property of that type provided, but errors are misleading - pointing to property I want to specify and giving no errors for those that are specified after first property. – Andyally Dec 08 '22 at 21:30
  • "accepted answer requires splitting interfaces" - how so? – Tobias S. Dec 08 '22 at 21:33
  • @TobiasS., it literally says "Not with a single interface, since types have no conditional logic and can't depend on each other, but you can by splitting the interfaces" https://stackoverflow.com/a/40510700/9186426 – Andyally Dec 08 '22 at 21:49
  • Oh sorry. I thought you meant the answer with the most votes. – Tobias S. Dec 08 '22 at 21:50
  • What is the result of the utility type you're looking for in your example? For something like `type Foo = { a: A, b: B }`, is it `({ a: A } | { b: B }) extends Util` or did I misunderstand? – Emma Dec 08 '22 at 21:52
  • @Emma, yes I want to have type Identifier with n properties, so in this case I would need to do type Foo = { n1 } | { n2 } | .... { n }, so I will need to update Foo type and add additional union...This will work but I am looking for alternative which will allow only update type Identifiers and have another type something like OnlyOne. – Andyally Dec 08 '22 at 22:12

2 Answers2

1

To create a utility type that takes in a type with multiple properties and produces an union of types where each individual member corresponds to one of the keys of the original multi-property type, you could do something like this:

type Split<T> = { [K in keyof T]: Pick<T, K> }[keyof T]

This will mean the following:

type Foo = {
  a: A,
  b: B,
  c: C
}

type Bar = Split<Foo>
//   ~~~
//   Bar = Pick<Foo, "a"> | Pick<Foo, "b"> | Pick<Foo, "c">
//       = { a: A }       | { b: B }       | { c: C }

Now, I understand you want IntelliSense to not suggest the other properties, like it does here:

type Foo = { a: number, b: string }
const foo: Split<Foo> = { a: 0, … }
//                              ^
//                              | + (property) b: string

But I'm not aware of a way to make TypeScript totally ignore the existence of b: String, since { a: number, b: string } extends { a: number } | { b: string }; as long as the shape of a value satisfies the type assigned to it, TypeScript will allow it. The TypeScript language service in your editor is "smart enough" to know to suggest the possible properties to you — it's not directly related to the type system itself and you cannot influence it in this case.


Since you wanted to have the other properties set to never in the union, you could do something like:

type Split<T> = {
  [K in keyof T]:
    Pick<T, K> &
    Partial<
      Record<
        Exclude<keyof T, K>,
        never
      >
    >;
}[keyof T]

Now you'll have:

type Foo = {
  a: A,
  b: B,
  c: C
}

type Bar = Split<Foo>
//   ~~~
//   Bar = (Pick<Foo, "a"> & Partial<Record<"b" | "c", never>>)
//       | (Pick<Foo, "b"> & Partial<Record<"a" | "c", never>>)
//       | (Pick<Foo, "c"> & Partial<Record<"a" | "b", never>>)
//       = ({ a: A }       & { b?: never, c?: never })
//       | ({ b: B }       & { a?: never, c?: never })
//       | ({ c: C }       & { a?: never, b?: never })
//       = ({ a: A, b?: never, c?: never })
//       | ({ b: B, a?: never, c?: never })
//       | ({ c: C, a?: never, b?: never })

At least with the current TSC version, the errors this approach produces seem to be misleading and generally hard to read anyhow. I found another post with this problem addressed by Jcalz; they have a more elegant solution: https://stackoverflow.com/a/57576688/11308378

Emma
  • 637
  • 6
  • 15
  • Thanks, but this allowed me to pass all props of Identifiers without error. – Andyally Dec 08 '22 at 22:46
  • @Andyally So you _do_ want the other properties set to `never` after all? If the other properties are _not_ set to never, _any_ object with one of the required properties is considered to conform to the structure of the type! – Emma Dec 08 '22 at 22:54
  • @Andyally TypeScript does have strict object literal checking (aka freshness checking) but that will ensure object literals _only_ contain _known_ property names. Since the properties of the `Identifiers` type in your example _are known_, you'll have to either accept the solution utilising `never` or then allow types that `extend` the union in my solution. TypeScript only requires that all required properties exist in a value, not that extra ones don't, unless you explicitly specify it with `never`. – Emma Dec 08 '22 at 23:04
  • Understood. I am now thinking about some type that can map all properties to new types where that prop is required and other properties are set to never. Is that possible? Something that transforms type Main = {a: string; b: number} into {a: string; b?: never} | {a?: never; b: number} without explicitly defining this structure. OnlyOneMapped = ..... (something like in JS map function). – Andyally Dec 08 '22 at 23:11
  • @Andyally Sure! I'll add it to my answer. – Emma Dec 08 '22 at 23:21
  • Thank you, Emma. The only thing it still allows me to specify other props but only with value of undefined. That's not a big deal, just curious why error says " Type '{ customerId: number; personalId: number; }' is not assignable to type 'Partial>'. Types of property 'personalId' are incompatible. Type 'number' is not assignable to type 'undefined'. Instead never it gives undefined, so I still can specify property. Is that because of Partial? – Andyally Dec 08 '22 at 23:48
  • @Andyally Yeah I spotted that too. Stumps me but I managed to find an earlier solution to this situation; linked in the answer now. – Emma Dec 08 '22 at 23:53
0

To create a type that allows only one of the properties from the Identifiers type to be specified, you can use the Pick type from TypeScript. Pick takes a type and one or more keys from that type, and creates a new type that includes only the specified keys.

For example, if you want to create a type that allows only the alias property from Identifiers to be specified, you can use Pick like this:

type AliasIdentifier = Pick<Identifiers, "alias">;

You can then use this new AliasIdentifier type wherever you need to specify only the alias property from Identifiers. For example, you could use it as the type for a React component prop like this:

function MyComponent(props: { identifier: AliasIdentifier }) {
  // ...
}

Now, if you try to pass an Identifiers object to the identifier prop of MyComponent, TypeScript will give an error, because MyComponent expects an object with only the alias property, not the full Identifiers object.

Shamoon
  • 41,293
  • 91
  • 306
  • 570
  • That's the catch that I don't know which prop from Identifiers will be passed. I want to leave this to developers who will use component and specify any of Identifiers prop but only one. – Andyally Dec 08 '22 at 22:36