How to hint the type of a function I do not control?

Viewed 168

When parsing a JSON-formatted string I get a linter error:

let mqttMessage = JSON.parse(message.toString())

// ESLint: Unsafe assignment of an `any` value. (@typescript-eslint/no-unsafe-assignment)

I control the content of message so I would like to tell TS that what comes out of JSON.parse() is actually an Object. How can I do that?

Note: I could silence the warning, but I would like to understand if there is a better way to approach the problem.

3 Answers

JSON.parse isn't generic, so we can't supply a generic argument to do it.

You have a couple of options.

The simple thing is that since JSON.parse returns any, you can just define the type of what you're assigning it to:

let mqttMessage: MQTTMessage = JSON.parse(message.toString());

(I've used MQTTMessage as a stand-in for the appropriate type.)

That may not be typesafe enough for everyone, though, since it makes the assumption that the string defines what you expect it to define. And it has the problem that if you do it elsewhere, you repeat the assumption.

Instead, you could define a function:

function parseMQTTMessageJSON(json: string): MQTTMessage {
    const x: object = JSON.parse(json);
    if (x && /*...appropriate checks for properties here...*/"someProp" in x) {
        return x as MQTTMessage;
    }
    throw new Error(`Incorrect JSON for 'MQTTMessage' type`);
}

Then your code is:

let mqttMessage = parseMQTTMessageJSON(message.toString());

The problem is that JSON.parse returns an any type.

That's fair enough right - TypeScript doesn't know if it's going to parse out to a string, a number, or an object.

You have a linting rule saying 'Don't allow assigning variables as any'.

So yeah, you could coerce the result of your JSON.parse

type SomeObjectIKnowAbout = {

}; 
const result = JSON.parse(message.toString()) as SomeObjectIKnowAbout; 

What I tend to like doing in this scenario is create a specific parsing function, that will assert at runtime that the obj really is of the shape you are saying, and will do the type casting to you can treat it while you're writing your code as that object.

type SomeObjectIKnowAbout = {
    userId: string; 
}

type ToStringable = {
    toString: () => string; 
}

function parseMessage(message: ToStringable ) : SomeObjectIKnowAbout {
    const obj = JSON.parse(message.toString()); //I'm not sure why you are parsing after toStringing tbh. 

    if (typeof obj === 'object' && obj.userId && typeof obj.userId === 'string') {
        return obj as SomeObjectIKnowAbout; 
    }
    else {
        throw new Error ("message was not a valid SomeObjectIKnowAbout"); 
    }
}


As an alternative to type assertions and runtime wrapper functions, you can utilize declaration merging to augment the global JSON object with a generic overload for the parse method. This will allow you to pass through the expected type and give you improved IntelliSense in case you use a reviver when parsing:

interface JSON {
    parse<T = unknown>(text: string, reviver?: (this: any, key: keyof T & string, value: T[keyof T]) => unknown): T
}

type Test = { a: 1, b: "", c: false };

const { a, b, c } = JSON.parse<Test>(
    "{\"a\":1,\"b\":\"\",\"c\":false}",  
    //k is "a"|"b"|"c", v is false | "" | 1
    (k,v) => v
);

Or, if you are relying on declaration files to augment global interfaces:

declare global {
  interface JSON {
    parse<T = unknown>(text: string, reviver?: (this: any, key: keyof T & string, 
  value: T[keyof T]) => unknown): T
  }
}

Playground

Related