Skip to content

How to implement n-ary Extract? #23413

Closed
Closed

Description

@bcherny

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>>

Activity

  1. jack-williams commented on Apr 14, 2018

    @jack-williams
    Collaborator

    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>;
  2. kpdonn commented on Apr 15, 2018

    @kpdonn
    Contributor

    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.

  3. bcherny commented on Apr 22, 2018

    @bcherny
    Author

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

  4. kpdonn commented on Apr 23, 2018

    @kpdonn
    Contributor

    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.

  5. typescript-bot commented on May 8, 2018

    @typescript-bot
    Contributor

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

  6. locked and limited conversation to collaborators on Jul 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions