1

This question starts off from the answer to this question:

https://stackoverflow.com/a/60807986/13809150

Is there a way to make an array of SingleKey with all different keys? Doing Array<SingleKey<T>> OR SingleKey<T>[] makes it so they all need the same key. (e.g. [{key1: "value1"},{key2:"value1"}] throws and error because key1!==key2. Which is sadly the exact opposite behavior that I am looking for.

This is what I am trying to do/have done:

// From https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
// From: https://stackoverflow.com/a/53955431
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type SingleKey<T> = IsUnion<keyof T> extends true ? never : {} extends T ? never : T;

function f<T extends Record<string, any>>(obj: SingleKey<T>[]) {
    // console.log({ obj });
}

f([{}]); // errors here! This is correct
f([{ x: 5 }, { x: 5 }]); // Should have error because same key (NEED TO FIX)
f([{ x: 5 }, { y: 6 }]); // Should not have error because of opposite keys (NEED TO FIX)
f([{ x: 5}, { y : 6 }, { y : 4}]) // Should  fail because duplicate key

The above code in a ts playground

Daniel
  • 1,392
  • 1
  • 5
  • 16
  • Seems like changing line 6 from `extends true` to `extends false` accomplishes what you want? – Hamza Kubba Mar 14 '22 at 03:10
  • I know this wasn't in the tests, but this fails (does not have error) when it is like `f([{ x: 5}, { y : 6 }, { y : 6}])` Updated the link and question to reflect it. Appreciate the help though! – Daniel Mar 14 '22 at 03:13
  • Potentially the rest (...) or Tail<> operators may come in handy here, but this is beyond my TypeScript skill unfortunately! – Hamza Kubba Mar 14 '22 at 04:41
  • @HamzaKubba Haha no worries, it was beyond mine too! – Daniel Mar 16 '22 at 00:33

1 Answers1

2

I don't think that this is possible without using tuple types and casting your arrays to readonly tuples. The reason for this is because you don't know if any type in the union of the generic is repeated to ensure that { y: number } is repeated over and over again because the type is simply { x: number } | { y: number }.

f([{ x: 5}, { y : 6 }, { y : 4}])

However, here is my solution if you are ok with using recursive tuple mapper types and casting your arrays to readonly via as const:

/** A helper type to enforce constraints in conditional type clauses. */
type Is<T extends K, K> = T;

type EnsureSingleKey<
  T extends readonly Record<string, unknown>[],
  $Acc extends Record<string, unknown> = {},
> = T extends readonly [
  Is<infer $First, Record<string, unknown>>,
  ...Is<infer $Rest, readonly Record<string, unknown>[]>,
] ? keyof $First extends keyof $Acc ? never
: EnsureSingleKey<$Rest, $Acc & $First>
  : true;

function f<T extends readonly Record<string, unknown>[]>(
  _obj: EnsureSingleKey<T> extends never ? never : T,
) {
}

// Argument of type 'readonly [{}]' is not assignable to parameter of type 'never'.
f([{}] as const);

// Argument of type 'readonly [{ readonly x: 5; }, { readonly x: 5; }]' is not assignable to parameter of type 'never'.
f([{ x: 5 }, { x: 5 }] as const);

// Argument of type 'readonly [{ readonly x: 5; }, { readonly y: 6; }, { readonly y: 4; }]' is not assignable to parameter of type 'never'.
f([{ x: 5 }, { y: 6 }, { y: 4 }] as const);

// Correctly passes!
f([{ x: 5 }, { y: 6 }] as const);

TypeScript Playground Link

sno2
  • 3,274
  • 11
  • 37
  • I mean I don't personally mind using const, but the only bad part about it is that it is easy to forget to add it to the function call, and then it's a runtime error. – Daniel Mar 16 '22 at 07:42