Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add type safety when passing functions as arguments?

I'm curious as to how to correctly type function parameters, regarding generics.

Looking at the following basic example, I have 2 functions: - sayHello & runAnyFunction

Now, the first call to sayHello is clearly an error, the data passed to the function doesn't have a name parameter. Perfect and expected.

The issue is with the next part, where we have a function runAnyFunction that takes a function as its first parameter, obj as its second parameter, and simply runs the function passing obj as the argument.

The following 2 functions in the sample below will pass typescript checks no problem, and that is my issue.

runAnyFunction<Person>(sayHello, {
  name: "Bob"
});

runAnyFunction<Monster>(sayHello, {
  size: "HUGE"
});

I know that the second call will give me a Hello undefined, which is undesired. Ideally I'd like this call to fail type checking as this shouldn't be allowed.

My thinking is that the culprit here is the fn function parameter, as its own obj parameter will be inferred as any, but I'm having difficulty writing the correct typing for this.

I understand that in this demo, my runAnyFunction is a function that really just runs any function, but if there's a preferred way to type a function of this nature, I would love to know.

How can I correctly type this?


Code Sample

// demo.ts

type Fn = (obj) => string;

interface Person {
  name: string;
}

interface Monster {
  size: string;
}

const somePerson: Person = {
  name: "Bob"
};

const someMonster: Monster = {
  size: "HUGE"
};


sayHello(someMonster); // ERROR, 'name' is missing in type. Sure, expected.


// sayHello requires an obj of type Person
export function sayHello(obj: Person): string {
  return `Hello ${obj.name}`;
}

// Simple function that runs any function, passing obj to it
export function runAnyFunction<T>(fn: Fn, obj: T): void {
  const res = fn(obj);
  console.log(res);
}

// Person has name, this works
runAnyFunction<Person>(sayHello, {
  name: "Bob"
});

// I'm passing sayHello, which should require an object with a `name`
// but it does not error
runAnyFunction<Monster>(sayHello, {
  size: "HUGE"
});

like image 855
jkusachi Avatar asked Oct 17 '22 05:10

jkusachi


1 Answers

You need to make your Fn type generic too:

type Fn<T> = (obj: T) => string;
export function runAnyFunction<T>(fn: Fn<T>, obj: T): void {
    const res = fn(obj);
    console.log(res);
}

The reason that you didn't receive an error before is that your Fn type implicity had the any type for its first parameter:

type Fn = (obj) => string; // obj is implicitly of type any
like image 196
Shaun Luttin Avatar answered Oct 20 '22 23:10

Shaun Luttin