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

How to implement n-ary Extract? #23413

Closed
bcherny opened this issue Apr 14, 2018 · 5 comments
Closed

How to implement n-ary Extract? #23413

bcherny opened this issue Apr 14, 2018 · 5 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@bcherny
Copy link

bcherny commented Apr 14, 2018

I built a tiny library that makes mixins typesafe, including detecting method collisions at compile time. The implementation is really simple, and it's a nicer API than the one in the TS handbook, I think.

I'm having trouble extending it to support more than a few parameters passed to mixin(A, B, C), because I have to test all permutations for Extract to work:

  • 2-ary mixin():
    • Extract<keyof A, keyof B>
  • 3-ary mixin():
    • Extract<keyof A, keyof B | keyof C>
    • Extract<keyof A | keyof C, keyof B>
  • 4-ary mixin():
    • Extract<keyof A, keyof B | keyof C | keyof D>
    • Extract<keyof A | keyof D, keyof B | keyof C>
    • Extract<keyof A | keyof C, keyof B | keyof D>
    • Extract<keyof A | keyof C | keyof D, keyof B | keyof D>

This quickly explodes and gets really branchy, making the PreventCollisions type (below) really unwieldy.

Any suggestions for how to avoid this branchieness, to support more params? The thing I'd like to express is "What are the keys that shapes A, B, C ... Z have in common?"

The issue tracker is not the best place for questions like these, but I thought TS maintainers would be interested to see this solution. Feel free to close if you feel otherwise :)

Code is here: https://github.com/bcherny/magical-mixin/blob/master/src/index.ts.

Relevant section is:

type PreventCollisions<A, B, C = never> = Extract<keyof A, keyof B | keyof C> extends never
  ? Extract<keyof A | keyof C, keyof B> extends never
    ? Ctor<A & B & C>
    : 'Error' & Methods<Extract<keyof A | keyof C, keyof B>>
  : 'Error' & Methods<Extract<keyof A, keyof B | keyof C>>
@jack-williams
Copy link
Collaborator

jack-williams commented Apr 14, 2018

Not sure about this.. but maybe:

type BrandKey<T,Brand> = {[K in keyof T]: Brand}

type CheckKey<K, This, Brands> =
	K extends keyof Brands ?
	(This extends Brands[K] ? never : K)
	: never;

type Check<T extends [any,string], Brands> = T extends {} ?
	CheckKey<keyof T[0], T[1], Brands> : never;

type PreventCollisionsInner<A, B, C, D, E, F, G, H> =
	Check<
	[A, "A"] |
	[B, "B"] |
	[C, "C"] |
	[D, "D"] |
	[E, "E"] |
	[F, "F"] |
	[G, "G"] |
	[H, "H"],
	BrandKey<A, "A"> &
	BrandKey<B, "B"> &
	BrandKey<C, "C"> &
	BrandKey<D, "D"> &
	BrandKey<E, "E"> &
	BrandKey<F, "F"> &
	BrandKey<G, "G"> &
	BrandKey<H, "H">
	>;

type CatchError<T, Pass> = [T] extends [never] ? Pass : ("Error " & [T]);

type PreventCollisions<A = {}, B = {}, C = {}, D = {}, E = {}, F = {}, G ={}, H = {}> = 
	CatchError<PreventCollisionsInner<A, B, C, D, E, F, G, H>,
	A & B & C & D & E & F & G & H>;

@kpdonn
Copy link
Contributor

kpdonn commented Apr 15, 2018

I think you need variadic types to do this right, but I've experimented with the same idea and a hacky way to accomplish something very similar is:

type ArrayKeys = keyof any[]
type Indices<T> = Exclude<keyof T, ArrayKeys>
type GetUnionKeys<U> = U extends Record<infer K, any> ? K : never
type CombineUnion<U> = { [K in GetUnionKeys<U>]: U extends Record<K, infer T> ? T : never }
type Combine<T> = CombineUnion<T[Indices<T>]>

declare function combine<
  T extends object[] &
    {
      [K in Indices<T>]: {
        [K2 in keyof T[K]]: K2 extends GetUnionKeys<T[Exclude<Indices<T>, K>]> ? never : any
      }
    } & { "0": any }
    >(objectsToCombine: T): Combine<T>


const result1 = combine([{ foo: 534 }, { bar: "test" }])

const error1 = combine([{ foo: 534, dupKey: "dup1" }, { bar: "test", dupKey: "dup2" }])

Playground link

The key is the & { "0": any } part. It tricks typescript into inferring a tuple type so that you have access to the elements passed via their numeric literal index. Discovered it in #22679. Never got an answer on if it's something that can be depended on to work in the future, but it has worked since 2.0 if not earlier so at least it's been pretty stable.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Apr 16, 2018
@bcherny
Copy link
Author

bcherny commented Apr 22, 2018

@kpdonn Man, that is some crazy type level programming. What would this look like with variadic types?

@kpdonn
Copy link
Contributor

kpdonn commented Apr 23, 2018

One thing is you'd be able to use rest parameters instead of manually passing an array. Besides that I don't know for sure since they aren't implemented yet and most of the discussion about them in #5453 happened before conditional types or even mapped types existed, but I'm hoping they'll be similarly flexible to work with.

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants