1

Is there a possiblity to define a function more typesafe than this?

public addBusinessRule(targetProperty: string,
                dependentProperties: string[],
                callback: (dep0: any, dep1: any, ...)): void {
   // some logic...
   callback(..dependentProperties)
};

my goal is to get typechecking for the last 2 arguments. so if I pass in something like this:

this.addBusinessRule('mortage',
                     ['purchasePrice', 'investiture', 'ownFunds'],
                     (purchase: number, investiture: number, ownFunds: number) => {
    // some calculations
});

anyone got an idea how i could define the size of the string-array from the second argument, that has to match the numbers of arguments accepted from the callback function.

can i somehow solve this with generics or is probably just no solution for this.

If someone knows a better more exact question title please request an edit!

2
  • Where so the types of the properties come from ? Is there some type taht has a 'mortage' that is an object with the properties ['purchasePrice', 'investiture', 'ownFunds'] (an maybe other) ? Commented Mar 7, 2019 at 9:53
  • yep something like this we got in the background a model with a propertery mortage this will be injected dynamically into the function. Commented Mar 7, 2019 at 10:12

2 Answers 2

2

You can use mapped tuples to transform the tuple of properties into a tuple of property types which can then be used as the arguments to the callback. This will ensure the callback can have at most the same number of parameters as items in dependentProperties. It will not force you to specify all arguments (this is how typescript defines type compatibility for functions).

type Data = {
    mortage: {
        purchasePrice: number,
        investiture: number,
        ownFunds: number,
        otherProp: string
    }
}
type MapTuple<T, K extends keyof T, NK extends Array<keyof T[K]>> = {
    [P in keyof NK]: NK[P] extends keyof T[K] ? T[K][NK[P]] : never
}
class Test {
    public addBusinessRule<K extends keyof Data, NK extends Array<keyof Data[K]>>(targetProperty: K,
        dependentProperties: NK | [], // Get the compiler to infer tuple types
        callback: (...a: MapTuple<Data, K, NK>) => void): void {
        // some logic...
        //callback(..dependentProperties)
    };
    public m() {
        this.addBusinessRule('mortage',
            ['purchasePrice', 'investiture', 'ownFunds', 'otherProp'],
            (purchase, investiture, ownFunds, op) => { // param types infered based on Data typr

            });
    }

}

The magic happens in the MapTuple type. The type uses mapped types (which since 3.1 support tuples and arrays as well, see PR). This type takes a the tuple NK which is a tuple of keys of T[K], and looks up the type of each property in T[K] (this involves a conditional type because typescript can't figure out NK[P] is a key of T[K] although it is guaranteed to be)

If you just want the number of parameters to be checked and not their type (although I would argue that is a worse experience for your API consumers) you can do the following:

type MapTuple<NK extends Array<any>> = {
    [P in keyof NK]: any
}
class Test {
    public addBusinessRule<NK extends Array<string>>(targetProperty: string,
        dependentProperties: NK | [], // Get the compiler to infer tuple types
        callback: (...a: MapTuple<NK>) => void): void {
        // some logic...
        //callback(..dependentProperties)
    };
    public m() {
        this.addBusinessRule('mortage',
            ['purchasePrice', 'investiture', 'ownFunds', 'otherProp'],
            (purchase, investiture, ownFunds, op) => { // all of type any 

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

6 Comments

could you probably explain the maptuple i don't completly get what you're doing there...
@Synoon added some explanations, let me know if you need more info.
is there a possibility so we don't have to define type Data and make it somehow generic i don't need typechecking in first place: checking the right amount of properties would be enough
@Synoon added a version that just maps the tuple to a tuple of the same size typed as any. You should really look into making your code more type safe, I don't really endorse any solution that uses any.
i totally agree with you but i'm not the api provider just a guy who tries to improve the code a little bit, that function is so universally usable that it's almost impossible to check for the corrects types or you are always typeing a map for every function call where i'm not sure right now if that would be a little bit an overkill my co-dev are sadly not really into supertypesafe coding..
|
1

What about a simpler example, just for string typed arguments?

export const addBusinessRule = <T extends any[]>(targetProperty: string, dependentProperties: T, callback: (...a: T[]) => void) => {
  // some logic...
  callback(...dependentProperties)
}

addBusinessRule('asd', ['sdf', 'sdf', 'sdf'], (first, second, third) => {
  console.log('ABC')
})

It provides types for each argument.

And if we're gonna use incorrect callback, the TS will throw an error.

const callback = (a: number, b: string, c: boolean) => {
  // ...
}

addBusinessRule('asd', ['sdf', 'sdf', 'sdf'], callback)
TS2345: Argument of type '(a: number, b: string, c: boolean) => void' is not assignable to parameter of type '(...a: string[][]) => void'.   Types of parameters 'a' and 'a' are incompatible.     Type 'string[]' is not assignable to type 'number'.

2 Comments

the problem is callback can accept any type of argument not only strings. these values get evaluated like object['sdf'] and this returns any type it could be.. we will get same exception even if the type are correct...
Then, you need the example below. It's for all types :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.