Related
I want to write a function that modifies a method on a function prototype, like this:
function inject<
T,
O = {
[K in keyof T as T[K] extends (...args: any) => any ? K : never]: T[K];
},
K extends keyof O = keyof O,
F extends (...args: any) => any = O[K] // error
>(o: { prototype: T }, func_name: K, func: (ret: ReturnType<F>) => void) {}
But typescript reported an error saying type "O[K]" does not satisfy the constraint "(...args: any) => any".
How do I fix this, or should I write my code differently?
I don't know if this is the best or easiest way to do it, but I suppressed the error by changing the O generic.
function inject<
T,
O extends Record<any, (...args: any) => any> = {
[K in keyof T as T[K] extends (...args: any) => any ? K : never]: T[K] extends (...args: any) => any ? T[K] : never;
},
K extends keyof O = keyof O,
F extends (...args: any) => any = O[K]
>(o: { prototype: T }, func_name: K, func: (ret: ReturnType<F>) => void) {}
Part of the problem is that O could be an arbitrary value, so typescript has no reason to believe that O[K] will be a function. To fix this, O should extend an object with function values (just like K extends keyof O and is assigned to keyof O). The T[K] extends (...args: any) => any ? T[K] : never is necessary because I think typescript isn't smart enough to infer that the value has to be a function due to the never guard in the value. It needs to know that it's a function so it can fit the Record<any, (...args: any) => any> signature.
Your main problem is that your O type parameter has a default type argument but is not constrained to that type, and in any case, O does not appear anywhere in the types of your function parameters, so there is no inference site for it.
Ideally you want as few type parameters as possible and you want them to appear in your function parameters as directly as possible. If you have a type parameter X then it is most easily inferred from a parameter of type X (e.g., <X,>(x: X) => void); it can also be inferred from a parameter with a property of type X (e.g., <X,>(v: {v: X}) => void, or from a homomorphic mapped type on X (e.g., <X,>(v: {[K in keyof X]: ...X[K]...}) => void, see What does "homomorphic mapped type" mean? for more info). And more complicated things can also sometimes be used in inference, but the general rule is you want it to appear as directly as possible.
So I'd probably write inject()'s call signature as follows:
function inject<
K extends PropertyKey,
T extends Record<K, (...args: any) => any>
>(
o: { prototype: T },
func_name: K,
func: (ret: ReturnType<T[K]>) => void
) { }
Here K is the type of func_name, and can be inferred to be the string literal type of the func_name argument because it is constrained to PropertyKey which is string | number | symbol. And T is the type of the prototype property of o , where T is constrained to have a function property at key K (using the Record<K, V> utility type). So T will be inferred from the prototype property of the o argument, and there will be an error if o.prototype[func_name] isn't of a function type.
Finally, the type of func is a callback whose argument is ReturnType<T[K]> using the ReturnType<T> utility type. This will not be used to infer T or K (it's too complicated)... but that's okay because T and K should already be inferred from other arguments. Instead, the compiler will use ReturnType<T[K]> with the already-inferred T and K to contextually type the callback argument. That is, inference is going the other way here. Let's test it out:
class Foo {
bar() {
return 123;
}
baz() {
return "xyz"
}
qux = 10
}
inject(Foo, "bar", x => x / 2);
inject(Foo, "bar", x => x.toUpperCase()); // error!
// Property 'toUpperCase' does not exist on type 'number'
inject(Foo, "baz", x => x.toUpperCase());
inject(Foo, "qux", x => x) // error!
// Type 'number' is not assignable to type '(...args: any) => any'
Looks good. The compiler is happy about the first call, and it understands that x is of type number because Foo.prototype.bar is a number-returning function. The second call it complains because number doesn't have a toUpperCase. Then the third call is okay because new Foo().baz() returns a string. The fourth call fails because new Foo().qux isn't a function at all; it's a number (plus it won't be on the prototype at all, but TypeScript models prototypes as identical to class instances, for better or worse, so that's not something the compiler could catch).
Playground link to code
I want to write a call function:
function call<F extends (...arg: any) => any, P extends Parameters<F>>(
fn?: F,
...arg: P
): ReturnType<F> | undefined {
if (fn) return fn(...arg) // Type 'P' must have a '[Symbol.iterator]()' method that returns an iterator
}
const fn = (a: string) => {}
// automatically infer `fn` arguments type
call(fn, )
what should I do?
This is a known bug in TypeScript; see microsoft/TypeScript#36874. It looks like the problem is triggered by F extends (...arg: any) => any instead of F extends (...arg: any[]) => any. That is, your arg rest parameter is of the anything-at-all any type instead of the array-of-anything any[] type. The obvious workaround is therefore to change any to any[], which shouldn't be a problem because function rest parameters are essentially always arraylike:
function call<F extends (...arg: any[]) => any, P extends Parameters<F>>(
fn?: F, ...arg: P): ReturnType<F> | undefined {
return fn?.(...arg) // okay (I used optional chaining here to deal with missing fn)
}
Looks good!
That's the answer to the question as asked, although it is useful to note that generally speaking, the preferred way to do this is to make call() generic in P (the rest parameter type) and R (the return type), and not use the Parameters<T> and the ReturnType<T> utility types:
function call<P extends any[], R>(fn?: (...arg: P) => R, ...arg: P): R | undefined {
return fn?.(...arg); // okay
}
They are similar, but your approach ends up being unable to properly represent what happens when fn is itself generic:
const pair = <T, U>(left: T, right: U): [T, U] => [left, right];
const ret = call(pair, "abc", 123); // old version of call
// const ret: [any, any] | undefined ☹
whereas the approach I recommend can take advantage of the support for higher order type inference from generic functions:
const ret = call(pair, "abc", 123); // new version of call
// const ret: [string, number] | undefined 👍
If you can rewrite generics using conditional types like Parameters<T> and ReturnType<T>, it will sometimes improve the compiler's ability to produce desirable results.
Playground link to code
This is a tricky one, but the solution is not.
You have to ensure the typescript that args is iterable. You can do that like manually saying to him - Hey this will be an array, believe me
Working playground
function call<F extends (...arg: any) => any, P extends Parameters<F>>(
fn?: F,
...args: P
): ReturnType<F> | undefined {
if (fn){
// here is the ugly hint to typescript
return fn(...(args as any[]))
}
return undefined;
}
Have you considered using apply as a workaround?
function call<F extends (...arg: any) => any, P extends Parameters<F>>(
fn?: F,
...arg: P,
): ReturnType<F> | undefined {
if (fn) return fn.apply(undefined, arg);
return undefined;
}
Playground
I am trying to write a function that works on all of the JavaScript array types, e.g. on number[], Float32Array etc. It should return the same type that it gets as a parameter. A simple example would be:
function addOne<T>(a: T) : T {
return a.map((x: number) => x + 1);
}
The function should be able to use all methods common to all array types (not just map).
I also tried
type NumberArray<T> = T extends Array<number>
? Array<number>
: Float32Array; // other array types skipped for brevity
function addOne<T>(a: NumberArray<T>): NumberArray<T> {
return a.map((x: number) => x + 1);
}
but I get
TS2322: Type 'number[] | Float32Array' is not assignable to type 'NumberArray<T>'. Type 'number[]' is not assignable to type 'NumberArray<T>'.
What would the TypeScript signature of such a function be? I also want to be able to create several such function and pass them as a parameter to another function (all properly typed, of course). A trivial example would be:
function doSomethingWithArray(a, func) {
return func(a);
}
The type of a should define which signature of func is used.
I have no problems running this in JS, but when trying to add proper TS typing, the TS compiler complains (I am running with "strict": true compiler option).
TypeScript does not have a built-in NumericArray type of which Array<number> and Float32Array-et-cetera are subtypes that gives you access to all common methods. Nor can I think of a one-or-two line solution that will give that to you. Instead, if you really need this, I'd suggest you create your own type. For example:
interface NumericArray {
every(predicate: (value: number, index: number, array: this) => unknown, thisArg?: any): boolean;
fill(value: number, start?: number, end?: number): this;
filter(predicate: (value: number, index: number, array: this) => any, thisArg?: any): this;
find(predicate: (value: number, index: number, obj: this) => boolean, thisArg?: any): number | undefined;
findIndex(predicate: (value: number, index: number, obj: this) => boolean, thisArg?: any): number;
forEach(callbackfn: (value: number, index: number, array: this) => void, thisArg?: any): void;
indexOf(searchElement: number, fromIndex?: number): number;
join(separator?: string): string;
lastIndexOf(searchElement: number, fromIndex?: number): number;
readonly length: number;
map(callbackfn: (value: number, index: number, array: this) => number, thisArg?: any): this;
reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number): number;
reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number, initialValue: number): number;
reduce<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: this) => U, initialValue: U): U;
reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number): number;
reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number, initialValue: number): number;
reduceRight<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: this) => U, initialValue: U): U;
reverse(): this;
slice(start?: number, end?: number): this;
some(predicate: (value: number, index: number, array: this) => unknown, thisArg?: any): boolean;
sort(compareFn?: (a: number, b: number) => number): this;
toLocaleString(): string;
toString(): string;
[index: number]: number;
}
That's kind of long, but I created it by merging the existing array typings in lib.es5.d.ts, and changing any reference to the particular array type with the polymorphic this type, meaning "the current subtype of the NumericArray interface". So, for example, Array<number>'s map() method returns an Array<number>, while a Float32Array's map() method returns a Float32Array. The this type can be used to represent this relationship between the array type and the return type.
If you care about post-ES5 functionality you can go and merge those methods in also, but this should be enough to demonstrate the basic approach.
You could try to write something that computes NumericArray programmatically, but I wouldn't want to. It is likely to be more fragile and more confusing than the manual NumericArray definition above, and probably take nearly as many lines.
Then, I'd write your addOne() in terms of NumericArray:
function addOne<T extends NumericArray>(a: T): T {
return a.map((x: number) => x + 1);
}
And you can verify that it works as expected for Array<number> and Float32Array:
const arr = addOne([1, 2, 3]);
// const arr: number[]
console.log(arr); // [2, 3, 4];
arr.unshift(1); // okay
const float32Arr = addOne(new Float32Array([1, 2, 3]));
// const float32Arr: this
console.log(float32Arr) // this: {0: 2, 1: 3, 2: 4}
console.log(float32Arr.buffer.byteLength); // 12
And your doSomethingWithArray would look like this:
function doSomethingWithArray<T extends NumericArray, R>(a: T, func: (a: T) => R) {
return func(a);
}
console.log(doSomethingWithArray([4, 5, 6], x => x.unshift(3))); // 4
console.log(doSomethingWithArray(new Int8Array([1, 2, 3, 4, 5]), x => x.byteLength)); // 5
Looks good!
Playground link to code
I was reminded of this question today and came up with something I find interesting:
function addOne <T extends NumericArray[number]> (a: T)
: Narrow<T, NumericArray> {
return a.map(x => x + 1) }
type NumericArray = [
Array<number>,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array
];
type Narrow<
T, L extends unknown[] = [],
R1 extends unknown[] = {
[K in keyof L]: T extends L[K] ? { type: T } : never
},
R2 = unknown extends T ? never : R1[number],
> = [R2] extends [{ type: unknown }] ? R2['type'] : never
I like how it has a different maintainability profile than jcalz's answer, which I still believe made more sense for your use case because the API is cleaner and you had to write a lot of functions for the same set of variants. That was a lot of grunt work though and I would see myself reaching for this second solution if I had to do this for different sets of variants.
I guess the API could be a little cleaner if Narrow took a union instead of a tuple but I don't know how computationally intensive that would be to add this step.
Your last question has a simple solution
function apply<T, U>(a: T, func: (x: T) => U): U {
return func(a);
}
UPDATE
This seems to work and I fail to see what is unsafe about it.
type NumberArray =
{ map:(f:(x:number) => number) => any }
& ArrayLike<number>
& Iterable<number>;
function addOne<T extends NumberArray>(a: T):T {
return a.map((x: number) => x + 1)
}
I don't think it's possible to do what you want with number[] and Float32Array because there is no such thing as a Float32 type in Javascript and we can't express something like "I want a generic Wrapper around a type Number | Float32 | ..." in Typescript anyway. Without these 2 features I don't see how it could be done.
We could do something like that which makes Typescript happy:
type NumberArray =
{
map: Function
// define other methods there
}
& ArrayLike<number>
& Iterable<number>;
function addOne<T extends NumberArray>(a: T): T {
return a.map((x: number) => x + 1)
}
But it allows you to map to a type other than number:
function addOne<T extends NumberArray>(a: T): T {
return a.map((x: number):string => x + '!') // woops
}
We could do the following:
type NumberArray =
{
map:(f:(x:number) => number) => NumberArray
// define other methods there
}
& ArrayLike<number>
& Iterable<number>;
function addOne<T extends NumberArray>(a: T): T {
return a.map((x: number) => x + 1)
}
but then we would get an error.
Type 'NumberArray' is not assignable to type 'T'.
'NumberArray' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ map: (f: (x: number) => number) => NumberArray; } & ArrayLike<number> & Iterable<number>
it can only be used like so, which looses type information (maybe it's OK for you though)
function addOne(a: NumberArray) {
return a.map((x: number):number => x + 1)
}
On a side note, I don't see the value of creating an addOne function which can only work with array-like objects. That's very specific. The whole point of map is to decouple the data structure from the transformation.
The following is more flexible because addOne doesn't need to know about where it's going to be used and the type information is preserved:
const addOne = (x: number) => x + 1;
addOne(3);
[1, 2, 3].map(addOne);
Float32Array.from([1,2,3]).map(addOne);
It doesn't solve your problem though: as soon as we define a function which needs to accept a NumberArray as argument we are back where we started...
Is there a way to set up a function based on an initial interface? I have a bunch of listOfFunctions and a an equal amount of interfaces that contain types of the functions. I would like to create a factory that returns a custom hook. The idea is to save a ton of code and make editing and adding features to all the hooks a breeze.
FunctionInterface Is all of the function names and their returns.
listOfFunctions is a setup function (the real code has a config) that returns a object with a list of functions.
theChosenFunction is a function that will be returned in the hook that allows me to interact with one of the listOfFunction functions. This means parameters (if needed). The functions in the real app are Promises that will set state of the hook. In this example, I just return a value and setState.
A) Is this possible to do with funcObject[funcName]() and create a function to invoke with typescript? This would need that ability to add params or not.
B) Is there a way to get the return value of a function type? So in this example:
type CanThisWork = () => string; I want to extract string; The idea is not to do a refactor on all of the listOfFunctions and FunctionInterfaces. There are a lot of them. If I have to create one complicated Factory, then I find this more economical.
interface FunctionInterface {
noParamsNoReturn: () => void;
noParamsNumberReturn: () => number;
propsAndNoReturn: (id: string) => void;
propsAndReturn: (id: string) => string;
}
const listOfFunctions = (): FunctionInterface => {
return {
noParamsNoReturn: () => void,
noParamsNumberReturn: () => 1,
propsAndNoReturn: (id: string) => console.log(id),
propsAndReturn: (id: string) => id,
}
}
function runThis<T>(funcName: keyof T, funcObject: T): void {
const [value, setValue] = React.useState();
// Can I have typescript set up the params here?
const theChosenFunction = ();
const theChosenFunction = (props) => {
// Can I have typescript invoke the function properly here?
const result = funcObject[funcName](); // funcObject[funcName](props)
if (result) {
setValue(result);
}
}
return {
theChosenFunction
}
}
const result = runThis<FunctionInterface>('noParamsNoReturn', listOfFunctions());
result.theChosenFunction()
// OR
result.theChosenFunction('someId');
There are built-in Parameters<T> and ReturnType<T> utility types that take a function type T and use conditional type inference to extract the tuple of parameters and the return type, respectively:
type SomeFuncType = (x: string, y: number) => boolean;
type SomeFuncParams = Parameters<SomeFuncType>; // [x: string, y: number]
type SomeFuncRet = ReturnType<SomeFuncType>; // boolean
As for your runThis() function,
I'd be inclined to give it the following typings and implementation, assuming you want the minimal changes that compile and run:
function runThis<K extends PropertyKey, F extends Record<K, (...args: any[]) => any>>(
funcName: K, funcObject: F
) {
const [value, setValue] = React.useState();
const theChosenFunction = (...props: Parameters<F[K]>): void => {
const result = funcObject[funcName](...props);
if (result) {
setValue(result);
}
}
return {
theChosenFunction
}
}
I've given the function two generic type parameters: K, corresponding to the type of funcName, and F, corresponding to the type of funcObject. The constraints K extends PropertyKey and F extends Record<K, (...args: any[])=>any> guarantee that funcName will be of a key-like type (string, number, or symbol), and that funcObject will have a function-valued property at that key.
Then, we can make theChosenFunction use spread and rest syntax to allow the function to be called with a variadic list of parameters. So (...props) => funcObject[funcName](...props) will accept any number of parameters (including zero) and pass them to the called function. TypeScript represents such lists-of-parameters as a tuple type. Therefore, having theChosenFunction's call signature look like (...props: Parameters<F[K]>) => void means that it will accept the same parameters as the entry in funcObject at the key funcName, and that it will not output anything (because your implementation doesn't output anything).
Let's see if it works:
const result = runThis('noParamsNoReturn', listOfFunctions());
result.theChosenFunction(); // okay
result.theChosenFunction('someId'); // error! Expected 0 arguments, but got 1.
const anotherResult = runThis('propsAndReturn', listOfFunctions());
anotherResult.theChosenFunction(); // error! Expected 1 arguments, but got 0.
anotherResult.theChosenFunction("someId"); // okay
runThis(listOfFunctions(), 'someId'); // error! '
// FunctionInterface' is not assignable to 'string | number | symbol'.
runThis('foo', listOfFunctions()); // error!
// Property 'foo' is missing in type 'FunctionInterface'
runThis(
'bar',
{ bar: (x: string, y: number, z: boolean) => { } }
).theChosenFunction("hey", 123, true); // okay
Looks good to me.
Playground link to code
Currently I have type definition as:
interface Param {
title: string;
callback: any;
}
I need something like:
interface Param {
title: string;
callback: function;
}
but the 2nd one is not being accepted.
The global type Function serves this purpose.
Additionally, if you intend to invoke this callback with 0 arguments and will ignore its return value, the type () => void matches all functions taking no arguments.
Typescript from v1.4 has the type keyword which declares a type alias (analogous to a typedef in C/C++). You can declare your callback type thus:
type CallbackFunction = () => void;
which declares a function that takes no arguments and returns nothing. A function that takes zero or more arguments of any type and returns nothing would be:
type CallbackFunctionVariadic = (...args: any[]) => void;
Then you can say, for example,
let callback: CallbackFunctionVariadic = function(...args: any[]) {
// do some stuff
};
If you want a function that takes an arbitrary number of arguments and returns anything (including void):
type CallbackFunctionVariadicAnyReturn = (...args: any[]) => any;
You can specify some mandatory arguments and then a set of additional arguments (say a string, a number and then a set of extra args) thus:
type CallbackFunctionSomeVariadic =
(arg1: string, arg2: number, ...args: any[]) => void;
This can be useful for things like EventEmitter handlers.
Functions can be typed as strongly as you like in this fashion, although you can get carried away and run into combinatoric problems if you try to nail everything down with a type alias.
Following from Ryan's answer, I think that the interface you are looking for is defined as follows:
interface Param {
title: string;
callback: () => void;
}
You can define a function type in interface in various ways,
general way:
export interface IParam {
title: string;
callback(arg1: number, arg2: number): number;
}
If you would like to use property syntax then,
export interface IParam {
title: string;
callback: (arg1: number, arg2: number) => number;
}
If you declare the function type first then,
type MyFnType = (arg1: number, arg2: number) => number;
export interface IParam {
title: string;
callback: MyFnType;
}
Using is very straight forward,
function callingFn(paramInfo: IParam):number {
let needToCall = true;
let result = 0;
if(needToCall){
result = paramInfo.callback(1,2);
}
return result;
}
You can declare a function type literal also , which mean a function can accept another function as it's parameter. parameterize function can be called as callback also.
export interface IParam{
title: string;
callback(lateCallFn?:
(arg1:number,arg2:number)=>number):number;
}
Here's an example of a function that accepts a callback
const sqk = (x: number, callback: ((_: number) => number)): number => {
// callback will receive a number and expected to return a number
return callback (x * x);
}
// here our callback will receive a number
sqk(5, function(x) {
console.log(x); // 25
return x; // we must return a number here
});
If you don't care about the return values of callbacks (most people don't know how to utilize them in any effective way), you can use void
const sqk = (x: number, callback: ((_: number) => void)): void => {
// callback will receive a number, we don't care what it returns
callback (x * x);
}
// here our callback will receive a number
sqk(5, function(x) {
console.log(x); // 25
// void
});
Note, the signature I used for the callback parameter ...
const sqk = (x: number, callback: ((_: number) => number)): number
I would say this is a TypeScript deficiency because we are expected to provide a name for the callback parameters. In this case I used _ because it's not usable inside the sqk function.
However, if you do this
// danger!! don't do this
const sqk = (x: number, callback: ((number) => number)): number
It's valid TypeScript, but it will interpreted as ...
// watch out! typescript will think it means ...
const sqk = (x: number, callback: ((number: any) => number)): number
Ie, TypeScript will think the parameter name is number and the implied type is any. This is obviously not what we intended, but alas, that is how TypeScript works.
So don't forget to provide the parameter names when typing your function parameters... stupid as it might seem.
There are four abstract function types, you can use them separately when you know your function will take an argument(s) or not, will return a data or not.
export declare type fEmptyVoid = () => void;
export declare type fEmptyReturn = () => any;
export declare type fArgVoid = (...args: any[]) => void;
export declare type fArgReturn = (...args: any[]) => any;
like this:
public isValid: fEmptyReturn = (): boolean => true;
public setStatus: fArgVoid = (status: boolean): void => this.status = status;
For use only one type as any function type we can combine all abstract types together, like this:
export declare type fFunction = fEmptyVoid | fEmptyReturn | fArgVoid | fArgReturn;
then use it like:
public isValid: fFunction = (): boolean => true;
public setStatus: fFunction = (status: boolean): void => this.status = status;
In the example above everything is correct. But the usage example in bellow is not correct from the point of view of most code editors.
// you can call this function with any type of function as argument
public callArgument(callback: fFunction) {
// but you will get editor error if call callback argument like this
callback();
}
Correct call for editors is like this:
public callArgument(callback: fFunction) {
// pay attention in this part, for fix editor(s) error
(callback as fFunction)();
}
There are various ways to define function types; however, some are better than others.
Although it is possible to use Function, the JavaScript function object, don't do that.
Source: TypeScript ESLint plugin recommended rule ban-types
Avoid the Function type, as it provides little safety for the following reasons:
It provides no type safety when calling the value, which means it's easy to provide the wrong arguments.
It accepts class declarations, which will fail when called, as they are called without the new keyword.
TypeScript supports multiple other ways. Most commonly, function type expressions are used. This method highly resembles arrow functions.
If arguments and return values are known to be of a certain form, then they should be typed.
For example,
interface Param {
callback: (foo: string, bar: number) => void
}
Note that these types can be as complex as needed, for example using object types or types created from other types.
If the types are truly unknown then prefer unknown over any.
Source: TypeScript ESLint plugin recommended rule no-explicit-any
When any is used, all compiler type checks around that value are ignored.
From the TS docs,
unknown is the type-safe counterpart of any.
Thus, using spread syntax,
interface Params {
callback: (...args: unknown[]) => unknown
}
Hopefully, this will help...
interface Param {
title: string;
callback: (error: Error, data: string) => void;
}
Or in a Function
let myfunction = (title: string, callback: (error: Error, data: string) => void): string => {
callback(new Error(`Error Message Here.`), "This is callback data.");
return title;
}
Typescript: How to define type for a function callback used in a method parameter?
You can declare the callback as 1) function property or 2) method:
interface ParamFnProp {
callback: (a: Animal) => void; // function property
}
interface ParamMethod {
callback(a: Animal): void; // method
}
There is an important typing difference since TS 2.6:
You get stronger ("sound") types in --strict or --strictFunctionTypes mode, when a function property is declared. Let's take an example:
const animalCallback = (a: Animal): void => { } // Animal is the base type for Dog
const dogCallback = (d: Dog): void => { }
// function property variant
const param11: ParamFnProp = { callback: dogCallback } // error: not assignable
const param12: ParamFnProp = { callback: animalCallback } // works
// method variant
const param2: ParamMethod = { callback: dogCallback } // now it works again ...
Technically spoken, methods are bivariant and function properties contravariant in their arguments under strictFunctionTypes. Methods are still checked more permissively (even if not sound) to be a bit more practical in combination with built-in types like Array.
Summary
There is a type difference between function property and method declaration
Choose a function property for stronger types, if possible
Playground sample code
I've just started using Typescript and I've been trying to solve a similar problem like this; how to tell the Typescript that I'm passing a callback without an interface.
After browsing a few answers on Stack Overflow and GitHub issues, I finally found a solution that may help anyone with the same problem.
A function's type can be defined with (arg0: type0) => returnType and we can use this type definition in another function's parameter list.
function runCallback(callback: (sum: number) => void, a: number, b: number): void {
callback(a + b);
}
// Another way of writing the function would be:
// let logSum: (sum: number) => void = function(sum: number): void {
// console.log(sum);
// };
function logSum(sum: number): void {
console.log(`The sum is ${sum}.`);
}
runCallback(logSum, 2, 2);
In typescript 4.8, Function type gives error. Instead we can write the type explicitly as fn: () => void.
If you want to use args as well,
function debounce(fn: (...args: any[]) => void, ms = 300) {
If you are looking for input callback function like on change event then use the following
type Props = {
callBack: ChangeEventHandler<HTMLInputElement>;
}