3

I have a union type of an Array of various specific lengths:

[ number ] | [ number, number ] | [ number, number, number, number ]

As you can see, there are requirements for an array with one element, two elements, or four elements.

I am trying to create an object which contains a function with one of these lengths. How do I write the type definition to allow for this?

TS playground

Example:

const people: {
    name: string,
    address: Address,
    work: (numbers: [ number ] | [ number, number ] | [ number, number, number, number ]) => any
}[] = [
    {
        name: "Bob",
        address: new Address(),
        work: function(numbers: [ number ]): number {
            // Implementation returning number
        }
    },
    {
        name: "Ashley",
        address: new Address(),
        work: function(numbers: [ number, number, number, number ]): boolean {
            // Implementation returning boolean
        }
    },
    {
        name: "Michael",
        address: new Address(),
        work: function(numbers: [ number, number ]): number {
            // Implementation returning number
        }
    },
]

Currently, it's giving me the error:

Error message

Type '(numbers: [number]) => number' is not assignable to type '(numbers: [number] | [number, number] | [number, number, number, number]) => any'. Types of parameters 'numbers' and 'numbers' are incompatible. Type '[number] | [number, number] | [number, number, number, number]' is not assignable to type '[number]'. Type '[number, number]' is not assignable to type '[number]'. Source has 2 element(s) but target allows only 1.ts(2322)

------- Edit -------

I've applied a suggestion from the comments and made all the possible calls into separate function unions instead of an array union:

const people: {
    name: string,
    address: Address,
    work: ((numbers: [ number ]) => any) | ((numbers: [ number, number ]) => any) | ((numbers: [ number, number, number, number ]) => any)
}[] = [

When trying to now call a function from this array:

 people[1].work([2, 8, 6, 4])

It throws the following error now:

Type number is not assignable to type never

In VSCode I found out this is why:

"The intersection '[number] & [number, number] & [number, number, number, number]' was reduced to 'never' because property 'length' has conflicting types in some constituents."

  • `work: ((numbers: [ number ]) => any) | ((numbers: [ number, number ]) => any)` – zoran404 Aug 31 '22 at 11:04
  • What you want to do is define all of the possible functions, instead of 1 function that can handle everything – zoran404 Aug 31 '22 at 11:05
  • Interesting. This is indeed the solution, but I thought it wouldn't work (for this reason: https://stackoverflow.com/questions/73389373/typescript-does-not-recognize-interface-function-expression-tuples). Please put your comment as an answer so I can set it as a valid answer. – DubiousMaster Aug 31 '22 at 11:07
  • Acually, I tried a bit more and it seems I still cannot call the functions. I get this error now: `The intersection '[number] & [number, number] & [number, number, number, number]' was reduced to 'never' because property 'length' has conflicting types in some constituents.` – DubiousMaster Aug 31 '22 at 11:27
  • You'll have to manually tell it the type of the function when calling it `(work as ((numbers: [ number ]) => any))([1])`, since typescript can't figure it out on it's own. – zoran404 Aug 31 '22 at 11:34
  • Or you can go back to your original definition for `work()`, but put the same union type for the `numbers` parameter on the implementation. Then in the implementation you'll have to check the length of the argument. – zoran404 Aug 31 '22 at 11:36
  • Can't I do something with Extract? – DubiousMaster Aug 31 '22 at 11:43

2 Answers2

2

UPDATED You need to use bivariance here

class Address { }

type Tuple<
  N extends number,
  Item = number,
  Result extends Array<unknown> = [],
  > =
  (Result['length'] extends N
    ? Result
    : Tuple<N, Item, [...Result, number]>
  )


interface WorkFn {
  work(numbers: Tuple<1> | Tuple<2> | Tuple<4>): any
}

interface Person extends WorkFn {
  name: string,
  address: Address,
}

const people: Person[] = [
  {
    name: "Bob",
    address: new Address(),
    work(numbers: Tuple<1>) {
      const [myNumber] = numbers;

      return myNumber * 6
    }
  },
  {
    name: "Ashley",
    address: new Address(),
    work: function (numbers: Tuple<4>): boolean {
      const [myNumber, anotherNumber, someNumber, replaceNumber] = numbers;

      return myNumber === anotherNumber && someNumber === replaceNumber;
    }
  },
  {
    name: "Michael",
    address: new Address(),
    work: function (numbers: Tuple<2>): number {
      const [myNumber, anotherNumber] = numbers;

      return myNumber * anotherNumber;
    }
  },
]

TypeScript playground

Here you can find the difference between method type an arrow function type and about bivariance

Also, please be aware that it is not 100% safe

  • 1
    Not a bad answer, but unfortunately, it does not allow me to actually call these functions. Turns out TypeScript converts the parameter type to 'never': `The intersection '[number] & [number, number] & [number, number, number, number]' was reduced to 'never' because property 'length' has conflicting types in some constituents.` – DubiousMaster Aug 31 '22 at 11:53
  • 1
    @DubiousMaster my bad, fixed – captain-yossarian from Ukraine Aug 31 '22 at 12:03
0

My proposal would be to use a generic function to create the people array.

function createPeople<
  T extends {
    name: string,
    address: Address,
    work: ((numbers: [ number ]) => any) | ((numbers: [ number, number ]) => any) | ((numbers: [number, number, number, number]) => any)
  }[]
>(p: [...T]){
  return p
}

const people = createPeople([
  {
    name: "Bob",
    address: new Address(),
    work: function(numbers: [ number ]): number {
      const [ myNumber ]: [ number ] = numbers;

      return myNumber * 6
    }
  },
  {
    name: "Ashley",
    address: new Address(),
    work: function(numbers: [ number, number, number, number ]): boolean {
      const [ myNumber, anotherNumber, someNumber, replaceNumber ]: [ number, number, number, number ]= numbers;

      return myNumber === anotherNumber && someNumber === replaceNumber;
    }
  },
  {
    name: "Michael",
    address: new Address(),
    work: function(numbers: [ number, number ]): number {
      const [ myNumber, anotherNumber ]: [ number, number ] = numbers;

      return myNumber * anotherNumber;
    }
  },
])

TypeScript now knows what the callback of each index is. That makes the following calls strictly typed.

people[1].work([2, 8, 6, 4])
people[2].work([1, 2])

Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45