Search code examples
javascripttypescriptobjecttsc

TypeScript does not correctly infer type?


Problem description

I want to have a function with generic return type which iterates through given object and do some operations with each of object values.

Example

I have a function:

function clearObject<T extends object>(obj: T): T {
    const newInstance = { ...obj };
    const keys = Object.keys(newInstance);
    keys.forEach(key => {
        if (typeof newInstance[key as keyof object] === 'function') {
            return;
        }

        if (typeof newInstance[key as keyof object] === 'object') {
            newInstance[key as keyof object] = clearObject<object>(newInstance[key as keyof object]);
        }
    });
    return obj;
}

On the line:

newInstance[key as keyof object] = clearObject<object>(newInstance[key as keyof object]);

TS detects an error:

TS2322: Type 'object' is not assignable to type 'never'.

Why? In above condition, I am verifying that newInstance[key] is of type 'object'. Is this issue of TypeScript? I am using the newest release, 4.9.


Solution

  • These sorts of recursive "rebuilds" of an object are very hard to get right while also keeping the type-system happy. Sometimes casts are inevitable.

    Rather than having to decide whether it is safe to recursively call your function, it's probably better to allow the function to accept any type and then to deal with it appropriately. As such:

    function clearObject<T>(obj: T): T {
        switch (typeof obj) {
            case "object":
                {
                    // null is an object
                    if (obj == null) {
                        return obj;
                    }
                    // arrays are objects
                    if (Array.isArray(obj)) {
                        return obj.map(clearObject) as T
                    }
                    // "normal" objects
                    return Object.fromEntries(
                        Object.entries(obj).map(([k, v]) => [k, clearObject(v)])
                    ) as T
                }
            default: {
                return obj;
            }
        }
    }
    

    should provide a nice way to recurse through whatever is passed.

    Playground Link