简体   繁体   中英

Typescript: specifying multiple callback types in a union

I am converting some stuff I use to typescript, and I'm at a bit of a loss. I made a generic foreach function that accepts arrays and objects, and takes a few different types of callbacks to handle the iteration. However, I don't know how to specify the type if it accepts a few different callbacks. I need to be able to return boolean or void from the function, and it should be able to pass in (any), (any, int), or (string, any). This is what I've got so far.

function foreach(obj : Array<any> | Object, func : (
            ((any) => boolean) |
            ((any) => void) |
            ((any, int) => boolean) |
            ((any, int) => void) |
            ((string, any) => boolean) |
            ((string, any) => void)
))
{
    // if obj is an array ...
    if(Object.prototype.toString.call(obj) === '[object Array]') {
        // if callback def1
        if(func.length == 1) {  // error: property length does not exist on type 'any[] | Object'
            for(let i = 0; i < obj.length; i++)  {
                if(typeof func === "function") {
                    if(!func(obj[i])) break; // error: Cannot invoke an expression whose type lacks a call signature
                }
            }
        // if callback def2
        } else if(func.length == 2) { // error: property length does not exist on type 'any[] | Object'
            for(let i = 0; i < obj.length; i++) {
                if(!func(obj[i], i)) break; // error: Cannot invoke an expression whose type lacks a call signature
            }
        }
    // if obj is an object ...
    } else if(Object.prototype.toString.call(obj) == '[object Object]') {
        // if callback def1
        if(func.length == 1) {
            for(let key in obj) {
                if(!obj.hasOwnProperty(key)) continue;
                if(!func(obj[key])) break; // error: Cannot invoke an expression whose type lacks a call signature
            }
        // if callback def3
        } else if(func.length == 2) {
            for(let key in obj) {
                if(!obj.hasOwnProperty(key)) continue;
                if(!func(key, obj[key])) break; // error: Cannot invoke an expression whose type lacks a call signature
            }
        }
    }
};

there you go:

function foreach<T>(obj : T[], func: (item: T) => void)
function foreach<T>(obj : T[], func: (item: T) => boolean)
function foreach<T>(obj : T[], func: (item: T, index: number) => boolean)
function foreach<T>(obj : T[], func: (item: T, index: number) => void)
function foreach<T>(obj : {[key: string]: T}, func: (item: T) => boolean)
function foreach<T>(obj : {[key: string]: T}, func: (item: T) => void)
function foreach<T>(obj : {[key: string]: T}, func: (key: string, item: T) => boolean)
function foreach<T>(obj : {[key: string]: T}, func: (key: string, item: T) => void)
function foreach<T>(obj : T[] | {[key: string]: T}, func : (item : T | string, index ?: number | T) => (boolean | void))
{
    if(Object.prototype.toString.call(obj) === '[object Array]') {
        let arr = <any>obj as T[];
        if(func.length == 1) {
            let cb = <any>func as (item: T) => boolean; 
            for(let i = 0; i < arr.length; i++) if(!cb(arr[i])) break;
        } else if(func.length == 2) {
            let cb = <any>func as (item: T, index: number) => boolean;
            for(let i = 0; i < arr.length; i++) if(!cb(obj[i], i)) break;
        }
    } else if(Object.prototype.toString.call(obj) == '[object Object]') {
        let arr = obj as {[key: string]: T};
        if(func.length == 1) {
            let cb = <any>func as (item: T) => boolean;
            for(let key in obj) {
                if(!obj.hasOwnProperty(key)) continue;
                if(!cb(obj[key])) break;
            }
        } else if(func.length == 2) {
            let cb = func as (key: string, item: T) => boolean;
            for(let key in obj) {
                if(!obj.hasOwnProperty(key)) continue;
                if(!cb(key, obj[key])) break;
            }
        }
    }
};

example of usage: 在此输入图像描述 在此输入图像描述

you can take advantage of intellisense: 在此输入图像描述

You can simplify your code by taking advantage of generic functions (functions parameterized by types, as in function foo<T> ), optional arguments (with ? ), and multiple function signature declarations. This will let you avoid all these unions and any 's.

First, it makes sense to break your function into two. The reason is that TypeScript does not support polymorphism in the sense of dispatching to different implementations at run time. You really have two different functions here (one for arrays and one for objects), and so you should write them as such. I know that jQuery and even underscore love pseudo-polymorphism, but actually this seeming convenience is a poor design principle. Write what you mean, and mean what you write.

Your code will end up looking like:

function forEachArray<T>(
  array: Array<T>, 
  func?: (value: T, index?: number): boolean
): void {
  for (let i = 0; i < obj.length; i++)  {
    if (func && !func(array[i], i)) break;
  }
}

function forEachObject<T>(
  object: {[index: string]: T}, 
  func?: (value: T, key?: string): boolean
): void {
  for (let key of Object.keys(object)) {
    if (func && !func(object[key], key)) break;
  }
}

Here we've used a couple of tools you seem perhaps not to have been aware of. One is the question mark ? to indicate optional arguments, which we use both for func itself, and also the second argument to func (the index or key). The second is generics , which we use here to enforce the homogeneity of the values in the array or object. This will enforce the notion that if your array contains strings, for example, the function you pass in must take a string as its first (value) parameter. if you want to relax this restriction, you can always call it as forEachObject<any>(... .

Design-wise, having the callback return either boolean or void seems questionable. Since you always use it as if it is returning a boolean, enforce that.

You do not need to special case the situation where the callback does or does not take a second parameter. You can simply make that parameter optional using ? in the signature for the func callback parameter, and go ahead and pass it, and the function will ignore it if it wants.

Also, for consistency with Array#forEach semantics, you should probably also allow an optional thisArgs third parameter, and also pass to your callback a third argument for the underlying array or object. In the case of forEachArray , that would look like this:

function forEachArray<T>(
  array: Array<T>, 
  func?: (value: T, index?: number, array?: Array<T>): boolean,
  thisArg?: Object
): void {
  for (let i = 0; i < obj.length; i++)  {
    if (func && !func.call(thisArg, array[i], i, array)) break;
  }
}

If you really want to have a single foreach function that takes either an array or an object, use the technique of defining multiple function signatures, followed by an implementation. I define a Hash type for compactness:

type Hash<T> = {[index: string]: T};

function foreach<T>(
  array: Array<T>, 
  func?: (value: T, index?: number, array?: Array<T>): boolean,
  thisArg?: Object
): void;

function foreach<T>(
  object: Hash<T>,
  func?: (value: T, key?: string, object?: Hash<T>): boolean,
  thisArg?: Object
): void;

function foreach(thing, func?, thisArg?) {
  (thing instanceof Array ? forEachArray : forEachObject)(thing, func, thisArg);
}

Note that when declaring multiple function signatures like this, you don't need to specify any types on the implementation, and they would be ignored if you did.

Caveat: none of the code above has been tested or even compiled.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM