Sun
Sun

Reputation: 319

Typescript: Array of a generic with different args

Consider a function that's called like:

func([
   {object: object1, key: someKeyOfObject1},
   {object: object2, key: someKeyOfObject2}
])

It's got an array. I want to enforce that the key field holds a key of the object that is held in object. Every object can have a different shape.

Constructing such a type for a single object is easy:

type Type<T> = { obj: T, key: keyof T }

However, I don't know how to create an array out of it, where every single element would be enforced. Type<any>[] would all drop types.

Upvotes: 1

Views: 566

Answers (2)

It is doable without extra functions but with small type overhead:

type Entity<Obj, Key> = {
    object: Obj,
    key: Key
}

type IsValid<T extends Entity<any, any>[]> = 
    /**
     * Infer each element of the array
     */
    T[number] extends infer Elem 
    /**
     * Check if every element of the array extends Entity
     */
    ? Elem extends Entity<any, any> 
    /**
     * Check if keyof Elem['object'] extends `key` property
     * 1) if [key] property is one of object properties - return true
     * 2) if at least one element does not meet your requirements return false | true,
     * because some element are ok
     */
    ? keyof Elem['object'] extends Elem['key'] 
    ? true 
    : false 
    : false 
    : false;

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type Validator<T extends boolean> =
    /**
     * If IsValid returns false | true (boolean) it means Error
     * otherwise - ok
     */
    IsUnion<T> extends true ?
    ['Dear developer, please do smth right']
    /**
     * I'm using empty array here, because 
     * (...flag:[])=>any evaluates to function without arguments
     */
    : []

const foo = <
    Value extends Record<PropertyKey, string>,
    Key extends keyof Value,
    Data extends Entity<Value, Key>[],
>(a: [...Data], ...flag: [...Validator<IsValid<[...Data]>>]) => a

/**
 * Ok
 */
foo([{
    object: { name: 'John' }, key: 'name'
},
{
    object: { surname: 'John' }, key: 'surname'
}])

/**
 * Error
 */
foo([{
    object: { name: 'John' }, key: 'name'
},
{
    object: { surname: 'John' }, key: 'name'
}])

This solution consists of two parts:

Part 1

We need to infer ach element of the array with help of variadic tuple types - Data generic. Here, in my article, you can find an explanation how to do it.

Part 2

We need to check if every element meets your requirements: Validator and IsValid type utils. More about this technique you can find in my blog here and here

Upvotes: 1

Oleksandr Kovalenko
Oleksandr Kovalenko

Reputation: 616

Restricting this on the function side is difficult. I don't even think there's a generic way to do that.

Non-generic solution: function overload

interface Item<TObject> {
  object: TObject
  key: keyof TObject
}

function func<T1, T2, T3, T4>(items: [Item<T1>, Item<T2>, Item<T3>, Item<T4>]): void
function func<T1, T2, T3>(items: [Item<T1>, Item<T2>, Item<T3>]): void
function func<T1, T2>(items: [Item<T1>, Item<T2>]): void
function func<T1>(items: Item<T1>[]): void {

}

func([
  { object: { a: '1' }, key: 'a' },
  { object: { b: '1' }, key: 'b' },
  { object: { c: '1' }, key: 'a' }, // not allowed
])

Solution 2: enforcing types on the calling side Basically, you rely on utility function. There's still way to make an error here and have compiler miss it (see last item in the example)

interface Item<TObject extends object> {
  object: TObject
  key: keyof TObject
}

function func(items: Item<any>[]) {

}

function createItem<T extends object>(object: T, key: keyof T): Item<T> {
  return {
    object,
    key
  }
}

func([
  createItem({ a: 1 }, 'a'),
  createItem({ b: 2 }, 'f'), // not allowed
  { object: { b: 2 }, key: 'f' }, // allowed
])

Solution 3: create a processor object with generic add method

interface Item<TObject> {
  object: TObject
  key: keyof TObject
}

function createMyArrayProcessor() {
  const array: Item<any>[] = []

  return {
    add<T>(item: Item<T>) {
      array.push(item)

      return this
    },
    result() {
      // process the array here and return the result
    }
  }
}

const result = createMyArrayProcessor()
  .add({ object: { a: '1' }, key: 'a' })
  .add({ object: { b: '1' }, key: 'b' })
  .add({ object: { c: '1' }, key: 'a' }) // not allowed
  .result()

Upvotes: 1

Related Questions