Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NonUnion type #100

Open
akwodkiewicz opened this issue Nov 18, 2019 · 7 comments
Open

Add NonUnion type #100

akwodkiewicz opened this issue Nov 18, 2019 · 7 comments
Labels
enhancement New feature or request v10.1

Comments

@akwodkiewicz
Copy link
Contributor

Summary

I would like to propose a NonUnion type:

type NonUnion<T> = [T] extends [UnionToIntersection<T>] ? T : never;

I've seen the concept in a couple of places [0] [1] and I've recently used it myself.

NonUnion<T> type resolves to T if T is not a union. Otherwise, it resolves to never.
By making sure that T is a particular union element, you can use index types for generic function params.

Example

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
  ? I
  : never;

type NonUnion<T> = [T] extends [UnionToIntersection<T>] ? T : never;

interface A {
  foo: "fooA";
  complex: {
    a: string;
  };
}
interface B {
  foo: "fooB";
  complex: {
    b: number;
  };
}
type AB = A | B;

const exampleA: A = {
  foo: "fooA",
  complex: {
    a: "some-data"
  }
};
const exampleB: B = {
  foo: "fooB",
  complex: {
    b: 42
  }
};

declare function simpleFn<T extends AB>(
  foo: T["foo"],
  complex: T["complex"],
): any;

// explicit generic type
simpleFn<A>(exampleA.foo, exampleA.complex);
simpleFn<B>(exampleB.foo, exampleB.complex);

// implicit `AB` type inferred
simpleFn(exampleA.foo, exampleA.complex);
simpleFn(exampleA.foo, exampleB.complex); // WRONG USAGE, no error

declare function nonUnionFn<
  T extends AB,
  U extends NonUnion<T> = NonUnion<T>,
>(foo: U["foo"], complex: U["complex"]): any;

// correct usage with explicit generic type
nonUnionFn<A>(exampleA.foo, exampleA.complex);
nonUnionFn<B>(exampleB.foo, exampleB.complex);

// wrong usage with explicit union type
nonUnionFn<AB>(exampleA.foo, exampleB.complex);
//             ^^^^^^^^^^^^
// compiler error: "Argument of type '"fooA"' is not assignable to parameter of type 'never'"
// fn resolved to: `nonUnionFn<AB, NonUnion<T>>(foo: NonUnion<T>['any'], complex: NonUnion<T>['complex]): any`

// wrong usage without explicit type
nonUnionFn(exampleA.foo, exampleB.complex);
//         ^^^^^^^^^^^^
// compiler error: "Argument of type '"fooA"' is not assignable to parameter of type 'never'"
// fn resolved to: `nonUnionFn<AB, never>(foo: never, complex: never): any`

// correct (!) usage without specifying generic type explicitly
nonUnionFn(exampleA.foo, exampleA.complex);
//         ^^^^^^^^^^^^
// compiler error: "Argument of type '"fooA"' is not assignable to parameter of type 'never'""
// fn resolved to: `nonUnionFn<AB, never>(foo: never, complex: never): any`

Notes

NonUnion<T> definition has to use [T] tuple, due to how distributive conditional types [2] work (naked T would be distributed to several definitions of T1 ? ... | T2 ? ... | ...).

References

[0] https://stackoverflow.com/a/50641073/7134149
[1] microsoft/TypeScript#32909
[2] https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types

@quezak
Copy link
Collaborator

quezak commented Nov 18, 2019

Rationale and code looks ok. Gratz for the additional research.

@krzkaczor @macbem @lucifer1004 what do you think? Does this look useful, is the name intuitive enough?

@macbem
Copy link
Contributor

macbem commented Nov 19, 2019

Looks great - IMO the name is clear enough and the type is quite useful.

@krzkaczor
Copy link
Collaborator

Great proposal 👍

Personally, I don't see anything wrong in:

simpleFn(exampleA.foo, exampleB.complex); // WRONG USAGE, no error

exampleA.foo and exampleB.complex will both be union types which totally makes sense if you ask me.

That being said I can imagine that there are some good use cases for such type and if people are searching for such type they apparently found some. It just would be cool if we could improve somehow rationale and provide good usecase for such type in docs 🤔

@quezak
Copy link
Collaborator

quezak commented Nov 19, 2019

exampleA.foo and exampleB.complex will both be union types

Yes, but from a general "union type". The goal is to e.g. enforce passing properties coming from the same particular "discriminated union subtype".
// note: this misunderstanding is mainly why I asked if the name is intuitive enough. Maybe OnlySpecificUnionSubtype? but that's probably too long

Usually I'd just pass Pick<T, 'foo', 'complex'> to achieve this instead of passing these two things separately, but it seems like for some people it's easier the proposed way.

@krzkaczor
Copy link
Collaborator

@quezak okay i think now i understand it better. Yes, the name is quite bad but with good examples it will be fine i guess.

Can we have good examples? :D Idk maybe renaming and implementing simpleFn function would make it easier to understand why it's important @akwodkiewicz

I think it was very similar with Buildable type - until good example appeared I was confused.

@quezak
Copy link
Collaborator

quezak commented Nov 19, 2019

I think @akwodkiewicz wanted to have a general name meaning "this is not a union type", but maybe we can think of a more specific one for the most common use case I see, meaning "this can be only one particular specific subtype of a union, not a few of them and not the whole union"

@quezak
Copy link
Collaborator

quezak commented Nov 19, 2019

Reworded example intro: if you have type U = A | B | C, you can call fn<A>(...) or fn<B>(...), but you can't call fn<U> nor fn<A|C>, this way you can for example enforce some arguments come from the same union subtype.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request v10.1
Projects
None yet
Development

No branches or pull requests

5 participants