14

I have read multiple posts and issues on here with respect to narrowing an object of type unknown in TypeScript. But I have not found this specific question or solution.

Most solutions I find state to do something like this.

const result: unknown = {
  movieName: "test",
};

if (
  typeof result === "object" &&
  result !== null &&
  "movieName" in result &&
  typeof result.movieName === "string" // <-- Error Here
) {}

The error states

Property 'movieName' does not exist on type 'object'

How do I narrow it so that it knows that the unknown thing is an object which contains the property movieName? How do I access result.movieName for an unknown type?

edit: perhaps it's not possible? and the recommendation in the comments below of declaring it as a Record<string, unknown> may be the only way per this GitHub request?

Diesel
  • 5,099
  • 7
  • 43
  • 81

1 Answers1

18

UPDATE FOR TS4.9+

TypeScript 4.9 will introduce support for in operator narrowing of unlisted properties, as implemented in microsoft/TypeScript#50666, as a fix for microsoft/TypeScript#21732 mentioned below. At that point the above code will just work without modification:

const result: unknown = {
    movieName: "test",
};

if (
    typeof result === "object" &&
    result !== null &&
    "movieName" in result &&
    typeof result.movieName === "string" // <-- okay
) {
    result.movieName.toUpperCase(); // <-- okay
}

Playground link to code


PREVIOUS ANSWER FOR TS4.8-

This is currently a missing feature in TypeScript. See microsoft/TypeScript#21732 for the relevant feature request. Right now when you use the in operator as a type guard like k in result, it only really does any meaningful narrowing if the type of result is a union where some members are known to have the key k and others are not. It does not assert the existence of a property with key k. So in your case, where the type of result is just object (narrowed from unknown), nothing meaningful happens.

The workaround I sometimes use is to write my own user-defined type guard function which behaves the way I want in to behave. For example:

function hasKey<K extends string, T extends object>(
    k: K, o: T
): o is T & Record<K, unknown> {
    return k in o;
}

Now, instead of writing k in result, I write hasKey(k, result):

if (
    typeof result === "object" &&
    result !== null &&
    hasKey("movieName", result) &&
    typeof result.movieName === "string" // okay
) {
    result.movieName.toUpperCase(); // okay
}

This works how you want, and is arguably the closest you can get to type safety here, at least until something is done to address microsoft/TypeScript#21732 natively.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • This is working well for me. Is there a way to combine the the first three steps into another type checking function? A function such as `isObjectWithKey` that checks that it is not null and is an object then runs hasKey? Thank you for the help. – Diesel Dec 11 '21 at 15:13
  • 3
    You could do [this](https://tsplay.dev/mbGb3W), sure. If you need anything further, you might consider posting a new questions, since comment sections of previous answers aren't always the best place to get followup answers. – jcalz Dec 12 '21 at 01:18