Search code examples
typescriptnarrowing

Narrow variable of type unknown into Record of unknown


I am trying to narrow a variable of type unknown to Record<PropertyKey, unknown> without using type assertions, but I am getting the following error;

Type 'object' is not assignable to type 'Record<PropertyKey, unknown>'. Index signature for type 'string' is missing in type '{}'.ts(2322)

How can I improve the following code without using a type assertion or custom type guard (like variable is Record<PropertyKey, unknown>) ? The only other question I found didn't appear to have a concrete answer.

const parseAsRecord = (variable: unknown): Record<PropertyKey, unknown> => {
  if (
    typeof variable !== "object" ||
    Array.isArray(variable) ||
    variable === null
  ) {
    throw new Error("Not Record");
  }

  return variable
};

I believe I need to tell the compiler that there is at least one property of either string, number or symbol on the narrowed object, but I'm not sure how.


And for posterity, if I need to also support the possibility of parsing an empty object, can I use the same signature, or would I need to do something like Record<PropertyKey, unknown> | {}? I ask because unknown[] neatly covers both an empty array and a populated array.


Solution

  • TypeScript currently does not narrow from the unknown type to Record<PropertyKey, unknown>. The closest you can get is narrowing to the object type, but since object is apparently not assignable to Record<PropertyKey, unknown>, this doesn't help you much. There is a longstanding open feature request at microsoft/TypeScript#38801 to allow the narrowing you're looking for, but it hasn't been implemented and apparently it might be a breaking change if it were, so for now it's not part of the language.

    You can always just assert than an object is a Record<PropertyKey, unknown>,

    const parseAsRecord = (variable: unknown): Record<PropertyKey, unknown> => {
        if (
            typeof variable !== "object" ||
            Array.isArray(variable) ||
            variable === null
        ) {
            throw new Error("Not Record");
        }
        return variable as Record<PropertyKey, unknown>
    };
    

    or, if you think you'll need such narrowing to happen in lots of places, you could refactor it to a custom type guard function which always returns true (since we presume that checking if a value is a non-null object is sufficient):

    function objectIsRecordUnknown(o: object): o is Record<PropertyKey, unknown> {
        return true; // this is effectively always true
    }
    
    const parseAsRecord = (variable: unknown): Record<PropertyKey, unknown> => {
        if (
            typeof variable !== "object" ||
            Array.isArray(variable) ||
            variable === null ||
            !objectIsRecordUnknown(variable) // no-op
        ) {
            throw new Error("Not Record");
        }
        return variable
    };
    

    Then objectIsRecordUnknown(variable) is always true, but now it allows TS to narrow from object as desired. If you're only doing the narrowing once, though, this is just a more roundabout way of using a type assertion (it's no safer).

    Playground link to code