2

I was using such approach for passing a props conditionally (depending on another prop) in react:

type CommonProps = { age: number };

type ConditionalProps =
  | {
      type: 'view' | 'add';
    }
  | {
      type: 'edit';
      initialData: number;
    };

let Test = (props: CommonProps & ConditionalProps) => {
  return <div />;
};

My goal was:

  • if type is passed as "view" or "add", then I want TS to error if I pass initialData
  • if type is passed as "edit" then initialData should be required.

This actually worked most of the time, until value of type was dynamic. When I passed value of type like this:

 <Test
   age={9}
   type={Math.random() > 0.5 ? 'edit' : 'view'}
   initialData={9}
 />

Now it seems there is bug above, because TS isn't complaining anymore, and it can happen that type is "view" and I also pass initialData, which violates my first goal above.

I am not sure maybe I am asking too much, but is there an approach which achieves my above two goals also when value of type is dynamic like in this example?

Giorgi Moniava
  • 27,046
  • 9
  • 53
  • 90
  • 2
    Please consider using `StrictUnion` helper. See [this](https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753) answer and my [article](https://catchts.com/unions). COmplete example [here](https://tsplay.dev/N9j1jm) – captain-yossarian from Ukraine Feb 13 '23 at 08:04
  • You may achieve this by the [_TypeScript Utility Types: Partial, Pick, and Omit_](https://www.skylerlemay.com/blog/2020/04/27/typescript-utility-types-part-1-partial-pick-and-omit). Or you can read the relevant discuss [here](https://stackoverflow.com/questions/71499966/create-type-having-common-props-from-other-types-and-other-optional). – haptn Feb 13 '23 at 08:47
  • @captain-yossarianfromUkraine solution from motto below seems simpler to me, does it have some drawbacks which yours doesn't have? (what I found bit weird in motto's answer is that it gives same error even if I use `string` instead of `never`; I had tried this version myself before but ignored due to this) – Giorgi Moniava Feb 13 '23 at 13:10
  • @GiorgiMoniava motto's solution is the same as mine. I have provided you with generic solution for all unions and motto provided you with example for your particular case. See result of `StrictUnion`, it also returns a union where disallowed property is `never` – captain-yossarian from Ukraine Feb 13 '23 at 14:19
  • 1
    @captain-yossarianfromUkraine when you linked [here](https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties), I started looking if it was exactly similar to my example. I think they are mainly, but maybe there is a small difference because I am using discriminated unions and that example doesn't. For example see here: https://t.ly/bo4y, you see `test2` doesn't error while `test1` does. Can you explain briefly what is difference there in terms of type checking there? Or maybe I should ask new question. – Giorgi Moniava Feb 13 '23 at 14:34
  • @GiorgiMoniava you are right, this is because `{A:1, C: 3} is assignable to {C: number}`. TS unions are tricky. This is why, I think that using discriminate unions is the safest option. If you can't use discriminator, the safest option would be to use `StrictUnion`, where all disallowed properties are `never` – captain-yossarian from Ukraine Feb 13 '23 at 15:06
  • @captain-yossarianfromUkraine yeah but I meant do you have idea why `test1` errors in my previous link? – Giorgi Moniava Feb 13 '23 at 16:26
  • @GiorgiMoniava this is because it is a different case. This union type has its own discriminator `type`. Consider [this](https://tsplay.dev/WvqKMm). `test1` evaluates to `{ type: "view" | "edit"; }`whereas object has to be either `{ type: 'view' }` or `{ initialData: number; type: 'edit'; }`. `type` can't be in the same type either `view` or `edit` because then TS is unable to figure out whether `initialData` should be present or not. If you add `initialData` it will work. Imagine that Math.random returns `0.6`, then object should contain `initialData`, but it will not – captain-yossarian from Ukraine Feb 13 '23 at 17:31
  • In one of the links above, a TS demo, I had a comment stating "Here there is no error . Probably because `{A:1, C: 3}` is assignable to `{C: number}`", which is not entirely true imho. Reason is because property `A` exists in one of the unions of type `T2`. – Giorgi Moniava Feb 14 '23 at 20:35

1 Answers1

2

In general, Typescript ignores excess properties unless they're explicitly assigned or passed as an argument.

So, although in your example here you're passing { age: number, type: "view", initialData: number }, that satisfies the type { type: "view" | "add" }. This is similar to the way that, for instance, you might ignore irrelevant fields in data from an external source.

If however you really want to ban the extra prop in this instance, you can do so by specifying that its type should be never:

type ConditionalProps =
  | {
      type: 'view' | 'add';
      initialData?: never;
    }
  | {
      type: 'edit';
      initialData: number;
    };

Note that we specify initialData as never and also optional, so that it may (must) be omitted.

With the never type in place, Typescript will report an error unless the type prop is "edit".

Minimal repro available here

motto
  • 2,888
  • 2
  • 2
  • 14
  • The extra prop is being banned even without using `never`, if I don't pass `type` as `Math.random() > 0.5 ? 'edit' : 'view'` and just use "view" or "edit". So why would I use `never`? – Giorgi Moniava Feb 13 '23 at 08:22
  • @GiorgiMoniava If you add `never` then it will forbid the extra prop – motto Feb 13 '23 at 08:28
  • it forbids that even if I use say `string` instead of `never`: [link](https://www.typescriptlang.org/play?ssl=8&ssc=21&pln=8&pc=27#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCmATzCTgGFcQIA7ABRzAGc4BeOAG84KAOZIAXHFYBXEACMkUOAF8A3BSq16TVgBNgMYGxQAbThB78KcW3AA+Qm3Zc06U-ADdgSAO74HAhQ9PXwNchcXYFZDYDMAERQYFAluGCho0XCXFWdbR0E8yLdJAiQDGDCiqJijBKSU6TlFKGy7dU0IilMkeAAVJDT+OAAKMC5uKSYQFg4JuAAyRjYK41YzCx4ASn4APicIuyIYGShWOAAeA084AHpd8I7KclvbuCUcKAo0NiGAD2GFwGaSKYiQfEEAE5codbCUIQBZJIACwAdFAUPpcCMdvsAAyogCscAA-GUKgEPN4-PgYZForF6skIdCKPcgA), with similar error message – Giorgi Moniava Feb 13 '23 at 08:31
  • @GiorgiMoniava I've linked through to a CodeSandbox, perhaps there are other differences in your code which are not reflected in your question. – motto Feb 13 '23 at 08:33
  • did you check my above link? It gives similar message with `string` instead of `never` – Giorgi Moniava Feb 13 '23 at 08:40
  • My apologies @GiorgiMoniava I did not see the inline link. The "similar error message" is indeed similar – when `type` can be `"edit" | "view"` then the value of the `initialData` prop must satisfy `number & string`, which is equivalent to `never`. – motto Feb 13 '23 at 08:42
  • But the error message doesn't say anything about `initialData`, it says " `Type '"view"' is not assignable to type '"edit"'`. Anyway, you can see it works in such case without `never`: https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCmATzCTgGFcQIA7ABRzAGc4BeOAG84KAOZIAXHFYBXEACMkUOAF8A3BSq16TVgBNgMYGxQAbThB78KcW3AA+Qm3Zc06U-ADdgSAO74HAhQ9PXwNchcVZ1tHQWiXODdJAiQDGDD4l2BWQ2AzABEUGBQpWQUlcMjwmwpTJHgAFSRueAEACjAubikmEBYOLrgAMkY2NONWMwseAEp+AD4nCLsiGBkoVjgAHgNPOAB6efD1TXJ9-bglHCgKNDYWuAAPfm2mlvixJD5BAE4o5dsSW+Xh8-n+CWyuQKRRQ3z+FEOQA – Giorgi Moniava Feb 13 '23 at 08:45
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/251828/discussion-between-motto-and-giorgi-moniava). – motto Feb 13 '23 at 08:46