1

I'm trying to type a function parameter where the param accepts any type where the union contains a known subset.

For example:

type One = { a: "one"; b: 1 };
type Two = { a: "two"; b: 2 };

type U = One | Two;

// `CouldContain` is some kind of wrapper that says it MIGHT contain `Two`
function test(param: CouldContain<Two>) {}

So the following would work:

const pass: U = { a: "one", b: 1 };

test(pass); // `U` contains `Two` 

And so would:

type Z = { } | Two

const pass: Z = { };
test(pass); // `Z` contains `Two`

But this wouldn't:

const fail = { a: "three", b: 3}

test(fail); // Not inferred as `Two`

Nor would:

const fail = { };

test(fail); // Valid branch of `Z` which contains `Two`, but no way to infer this as `Z` so it fails

The use-case I have is inferring whether dynamically generated GraphQL data types might contain a specific union type.

Lee Benson
  • 11,185
  • 6
  • 43
  • 57
  • Can you clarify this a bit? Is that you you want `CouldContain` to be the type of all type unions that include X ? or are you specifically dealing only with `U` here i.e. a specific type union? – apokryfos Jan 23 '23 at 23:08
  • @apokryfos, I've added another example to clarify. I need the param in `test` to be inferred to be a union that MIGHT contain `Two`. This could be any type that contains `Two` -- either static or inferred. – Lee Benson Jan 23 '23 at 23:14
  • 1
    @LeeBenson Although I could be overlooking some simpler syntax... does [this](https://tsplay.dev/WJ7bDw) answer your question? If it does, I can write it up as an answer. If not, what am I missing? – jsejcksn Jan 23 '23 at 23:43
  • are One Two Three errors types (as i understand in you comment) ? and are you working in a catch block ? – Romain TAILLANDIER Jan 24 '23 at 00:48
  • @jsejcksn Thanks, but unfortunately that doesn't work. Two things: a) I'm looking for call-site inference; casting makes this utility moot, sadly. b) In your example, casting to any matching type works, even if the object doesn't match! e.g. `type Bla = Two | {}; test({ anything: true} as Bla);` passes, even though the object matches neither `Bla` nor `Two`. – Lee Benson Jan 24 '23 at 00:51
  • @RomainTAILLANDIER, no, they're not error types. I'm using GraphQL Code Generator which creates complex dynamic types that can be unions of multiple different types (depending on the field my GraphQL query requests). What I'm trying to do is build some utility funcs that act on a subset of a document, and I want TS to reject any type which couldn't _possibly_ contain the subset – Lee Benson Jan 24 '23 at 00:53
  • [^](https://stackoverflow.com/questions/75215890/typescript-how-to-know-if-a-union-type-might-contain-a-subset#comment132727741_75215890) @LeeBenson Using [annotations](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-annotations-on-variables) doesn't prevent their narrowing by [inference](https://www.typescriptlang.org/docs/handbook/type-inference.html) at assignment sites: that kind of compiler override is accomplished by [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions). – jsejcksn Jan 26 '23 at 04:33
  • @jsejcksn yes, understood. What I'm trying to achieve seems impossible currently in Typescript. – Lee Benson Jan 26 '23 at 11:25

1 Answers1

1

What an odd question. Here is my approach.

type CouldContain<T, U> = Extract<T, U> extends never ? never : T

function test<T>(param: CouldContain<T, Two>) {}

This works by using the argument param to infer the generic type T and by using a conditional type to check if Two is a member of T.

When using this type, how have to note one important thing:

type U = One | Two;
const pass: U = { a: "one", b: 1 } as U;
test(pass); // ok

type Z = { } | Two
const pass2: Z = { } as Z;
test(pass2); // ok

const fail = { a: "three", b: 3}
test(fail); // fails

const fail2 = { };
test(fail2) // fails

The two type assertions as U and as Z are needed for this to work. Control flow analysis will otherwise use the literal type to attach invisible type information to pass and pass2. Without the type assertions the compiler slightly changes both types, so that Two is not part of their union anymore. The type assertions will stop the compiler from doing these optimizations.


Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • >> Without the type assertions the compiler slightly changes both types, so that Two is not part of their union anymore This is effectively what I'm trying to work around. I'm writing a library function that will accept any (implicitly typed) GraphQL response and type check it to contain a known 'error' type. If I have to cast the type beforehand, it significantly lessens the usefulness. Hmmm... thanks anyway! – Lee Benson Jan 24 '23 at 00:00
  • This is not a problem as long as you don't use object literals directly. Any form of indirection will avoid these type optimizations. – Tobias S. Jan 24 '23 at 00:01
  • probably usefulness, but just in case : what about help the generic method instead of infer : test(pass) ? – Romain TAILLANDIER Jan 24 '23 at 00:17