Typescript array-like type with same generic type between keys of each item

Viewed 333

I'd like to write a typescript function that accepts arguments like this:

myFunc([
  {
    initialValue: 6, // number
    finalValue: 8    // number
  },
  {
    initialValue: 'hello', // string
    finalValue: 'goodbye'  // string
  }
])

But will fail if given this:

myFunc([
  {
    initialValue: 6, // number
    finalValue: 'goodbye' // should fail because not a number!
  }
])

It feels like the solution should entail generics, but generics that are generic within each array item, not generic across the whole array.

EDIT: I'd like a solution that works with ANY type of value, not just strings or integers. I will likely need to use this for classes/functions as well.

6 Answers

You can get something like this to work using generics and mapped types. We will use a type parameter to capture the actual type of the parameter (whatever that is, even if is does contain invalid initial/final pairs). We then transform this type to a new type where finalValue is typed according to the actual initialValue passed in. We use this new type in with an intersection in the parameter type. This will mean that the type of the parameter is inferred into the type parameter but checked against out transformed type:

type FinalValues<T extends Array<{ initialValue: any }>> = {
    [P in keyof T]: T[P] extends { initialValue: infer I } ? { initialValue: I, finalValue: I }: never 
}

function myFunc<T extends [{ initialValue: any }] | Array<{ initialValue: any }>>(v: T & FinalValues<T>): FinalValues<T> {

}

myFunc([
  {
    initialValue: 6, // number
    finalValue: 8    // number
  },
  {
    initialValue: 'hello', // string
    finalValue: 'goodbye'  // string
  }
])

myFunc([
  {
    initialValue: 6, // number
    finalValue: "string"    // err
  },
  {
    initialValue: 'hello', // string
    finalValue: 1  // err
  }
])

Play

You could also be a bit more creative and add a sort of custom error so that the the errors are more readable: Play

Solution is to define list element type as a sum type which member or two strings or two numbers.

type A = {
  initialValue: number
  finalValue: number
}
type B = {
  initialValue: string
  finalValue: string
}

type ListAB = (A | B) [] // type contains or A(two numbers) or B(two strings)

Now the array with the type ListAB is strictly typed by Sum A | B. Example usage:


function myFunc(list: ListAB) {
  // implementation
}

// correct
myFunc([
  {
    initialValue: 6, // number
    finalValue: 8    // number
  },
  {
    initialValue: 'hello', // string
    finalValue: 'goodbye'  // string
  }
])

// will have error
myFunc([
  {
    initialValue: 6, // number
    finalValue: 'a'    // string
  },
  {
    initialValue: 1, // number
    finalValue: 'goodbye'  // string
  }
])

Maybe you can try something like below. I'm not 100% sure if it correct, but it works for the scenario which you have mentioned above.

interface type1 {
    arg1: Array<string>,
    arg2: Array<string>
}

interface type2 {
    arg1: Array<number>,
    arg2: Array<number>
}

var t = function Test(arg: type1 | type2) {
    alert(arg.arg1);
    alert(arg.arg2);
}

var arg = { arg1: [2], arg2: [1]};
t(arg);

Let me know your thoughts.

Using generics you can achieve this like so:

const myFn = <T>(args: { [p: string]: T }[]) => {

}

myFn<string>([
    {someArg: 'foo', someOtherArg: 'bar'}
])

myFn<number>([
    {
        someArg: 5,
        someOtherArg: 9,
    }
])

// ERROR
myFn<string>([
    {
        someArg: 'asdf',
        someOtherArg: 5,
    }
])

If you know your objects should only have specific arguments you can do the same thing with a more specific typing like so

const myFn = <T>(args: { initialValue: T, finalValue: T }[]) => {

}

Another way of achieving something like this is to use classes.

The downside is that it does not exactly replicate what you want, it comes pretty close though. The upside is that is is very straightforward and easy to understand.

class Arg<T> {
    initialValue: T
    finalValue: T

    constructor({ initialValue, finalValue }: { initialValue: T, finalValue: T }) {
        this.initialValue = initialValue
        this.finalValue = finalValue
    }
}

function myFunc(input: Arg<unknown>[]) {

}

myFunc([
    new Arg({
        initialValue: 6, // number
        finalValue: 8    // number
    }),
    new Arg({
        initialValue: 'hello', // string
        finalValue: 'goodbye'    // string
    }),
])

myFunc([
  new Arg({
    initialValue: 6, // number
    finalValue: 'string' // ERROR
  }),
])

I upvoted Maciej's post as it's complete and you probably want to get into defining your types.

Just to offer a quick and dirty solution along the same lines (this definitely seems like the way for me, simpler solution, straightforward). I would do types like Maciej though...

public myFunc(arr: ({ initialValue: string, finalValue: string } | { initialValue: number, finalValue: number })[])
Related