Ignore "unique symbol" in typescript when using "keyof typeof" with Object.keys()

Viewed 489

I am trying to display all the values in my current redux state but I am getting errors involving the unique symbol type and I am not sure how to resolve it.

Object.keys() says it's returning string[] but keyof typeof state includes unique symbol so .map() doesn't work. I added a .filter() clause but that didn't help.

This works fine when run in javascript but typescript screams and refuses to compile.

Simplified example:

const state = useAppSelector(state => state);
return (
    <div>
        {Object.keys(state).filter((x: keyof typeof state) => x instanceof String).map((x) =>
            <div>
                <label>{x}</label>
                <div>
                    {Object.keys(state[x]).filter((y: keyof typeof state[x]) => y instanceof String).map((y) =>
                        <span>{y}: {JSON.stringify(state[x][y])}</span>
                    )}
                </div>
            </div>
        )}
    </div>
);

How can I correctly type this?

EDIT:

I attempted to use the Omit<> utility type:

Object.keys(state).map((x: Omit<keyof typeof state, symbol>) =>

But this still results in an error when accessing the state via state[x]:

Type 'Omit<"auth" | "developer" | "errors" | "info" | unique symbol, symbol>' cannot be used as an index type. ts(2538)

1 Answers

Object.keys only returns strings according to the spec, so your .filter() call isn't doing anything to help. The reason why Object.keys() doesn't return keyof T[] is pretty thoroughly covered in this answer.


Having read the linked answer, and understanding that using keyof for these types of situations is unsafe, there are two approaches using Object.entries.

The first safe version is to cast the value types to unknown, and use a typeguard whenever you need the actual type:

// Object.entries throws on null/undefined
function isEntriesCallable(arg: T): arg is Record<any,unknown> {
    return arg !== null && typeof arg !== "undefined";
}

const state = useAppSelector(state => state);
return (
    <div>
        {Object.entries(state as Record<any, unknown>).map(([x, stateX]) =>
            <div>
                <label>{x}</label>
                <div>
                    { isEntriesCallable(stateX) ? Object.entries(stateX).map(([y, stateY]) =>
                        <span>{y}: {JSON.stringify(stateY)}</span>
                    ) : <span>null/undefined</span>}
                </div>
            </div>
        )}
    </div>
);

Pros/Cons are that all the value types are now unknown, and you have to call a typeguard if you want to narrow them. In your example, JSON.stringify accepts any, so having an unknown value doesn't actually impact anything. Any future edits to the code will force the developer to use typeguards if they want to call anything that requires a type besides any or unknown.


A less safe version suffers from the excess property issues highlighted in the linked answer. However, if you know the type is exact, this will narrow stateX to the union of the string-key property value types of typeof state. It uses a cast to a mapped type that does what you were intending with Omit. The provided typeguard also allows some type narrowing on the stateX value types to exclude values on stateX that are primitives or null so that you don't bother calling Object.entries on primitives/null.

// Extract an object containing only string property keys
// guarantees your type will match `{[key: string]: T}` for the
// argument to the Object.entries type definition.
type StringKeyedObject<T extends object, K extends keyof T = keyof T> = {
    [Key in (K extends string ? K : never)]:  T[Key];
}

type NonObject = number | string | boolean | symbol | bigint | undefined | null;
function isObjectLike<T extends object>(arg: T | NonObject): arg is T { return arg !== null && typeof arg === "object"; }

const state = useAppSelector(state => state);
return (
    <div>
        {Object.entries(state as StringKeyedObject<typeof state>).map(([x, stateX]) =>
            <div>
                <label>{x}</label>
                <div>
                    {isObjectLike(stateX) ? Object.entries(stateX as StringKeyedObject<typeof stateX>).map(([y, stateY]) =>
                        <span>{y}: {JSON.stringify(stateY)}</span>
                    ) : <span>{JSON.stringify(stateX)}</span>}
                </div>
            </div>
        )}
    </div>
);

Pros:

  • utilizes your own declared types for inference/narrowing.

Cons:

  • unsafe typecasts that may cause errors from unhandled object properties that don't exist on the type definition.
Related