3

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 6

3

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

Sign up to request clarification or add additional context in comments.

2 Comments

This is the best answer in my opinion
Agree, this is the best answer because it handles any type, not just strings or numbers
1

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
  }
])

1 Comment

I agree with your solution for being much simpler and more straightforward for other developers to read and understand.
0

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.

Comments

0

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 }[]) => {

}

3 Comments

In reading of the question, the request is to corelate initialValue with finalValue, they could be different between items but they have to be the same with an item. This will not work id the type is different between items
Yep, I noticed that on a second reading, that's why I upvoted your answer.
Wops, didn't notice that was you, my bad 😊
0

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
  }),
])

Comments

0

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 })[])

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.