1

I have a function that can take arguments of types A or B, which have some members in common but lack others. Within the body of the function, however, I'd like to be able to reference members that occur on only A or only B, with the understanding that they may be undefined at runtime.

An example looks like this:

type A = {
  common: number;
  a_only: string;
}

type B = {
  common: number;
  b_only: string;
}

type ArgType = A | B; // this doesn't actually work in the desired fashion

function takesAorB(arg: ArgType) {
  if (arg.common > 1) {
    // no need to check for undefined since both A and B have `common`
  }
  if (arg.a_only?.length) {
    // the effective type of `arg.a_only` should be `string | undefined`,
    // since the property exists on A but not B
  }
  if (arg.b_only?.length) {
    // the effective type of `arg.b_only` should be `string | undefined`
    // since the property exists on B but not A
  } 
}

I can get pretty close by doing something like this:

type ArgType = A | B | Partial<A> | Partial<B>

However, this makes every member of A and B optional. I would like it if the members that A and B have in common were still available as non-nullable. Is there any way to accomplish this?

JSBձոգչ
  • 40,684
  • 18
  • 101
  • 169
  • 1
    Does [this approach](https://tsplay.dev/m3B7yN) solve your problem? If yes, I will write an answer; If not, what am I missing? – wonderflame May 05 '23 at 15:23
  • 1
    If the shared members have the same type, https://tsplay.dev/wOlkRN – kelsny May 05 '23 at 15:30
  • 1
    @jcalz Added a concrete example which should hopefully illuminate what I want to do. – JSBձոգչ May 05 '23 at 15:53
  • 1
    Do you really mean *nullable*, or do you mean *`undefined`-able*? :-) – T.J. Crowder May 05 '23 at 15:54
  • Okay now this looks like a duplicate of [this question](https://stackoverflow.com/q/46370222/2887218), at least to the extent that you think `A` should actually *prohibit* the `b_only `property instead of being merely *unconcerned* about it. You can use the `ExclusifyUnion` utility type from my answer there to write `ExclusifyUnion` and it will probably do what you want. – jcalz May 05 '23 at 15:54
  • 1
    Something like [this](https://tsplay.dev/w1nZAN)? I've gone with "If the property only exists on A or only on B, it should be optional" (rather than `| null`) in `ArgType`. The key bit is the `MergeAsOptional` type. I wouldn't be surprised if it's more complicated than it should be, though. – T.J. Crowder May 05 '23 at 15:57
  • I would beware of this, though, since from a types perspective, whatever you pass in doesn't match the definition. In particular, if you pass in a `B` and the *assign to* an `A`-only property, that object shouldn't really have that property. Although excess properties are generally allowed, this seems a step over the line to me. – T.J. Crowder May 05 '23 at 15:58

1 Answers1

2

I think you're looking for something like this:

type ArgType =
    (A & {[Key in Exclude<keyof B, keyof A>]?: B[Key]}) |
    (B & {[Key in Exclude<keyof A, keyof B>]?: A[Key]});

That describes a union of two types:

  • A intersected with an object type with an optional property for every property in B that doesn't already exist in A. (Is the ? before the : after the key that makes it optional.)
  • B intersected with an object type with an optional property for every property in A that doesn't already exist in B.

I haven't tried to make any provision there for types where A and B have the same property name but differing types for it.

Instead of defining that directly, you could use a generic utility type to do it if you need to do this with other types:

// Utility type
type MergeAsOptional<A, B> =
    (A & {[Key in Exclude<keyof B, keyof A>]?: B[Key]}) |
    (B & {[Key in Exclude<keyof A, keyof B>]?: A[Key]});

// Using it
type ArgType = MergeAsOptional<A, B>;

Playground example

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875