mackenco
mackenco

Reputation: 13

Type for object from parsed string

I've got a function that parses a string into an object, casting numbers and booleans to their correct JS primitives (using this specifically for working with query params):

const parseQueryParams = (searchString: string) => {
  const params = new URLSearchParams(searchString).entries();

  return [...params].reduce((out, [key, value]) => {
    let parsed: string | boolean | number = value;
    if (!!Number(value) || value === '0') {
      parsed = Number(value);
    } else if (value === 'false') {
      parsed = false;
    } else if (value === 'true') {
      parsed = true;
    }

    return { ...out, [key]: parsed };
  });
};

parseQueryParams('?num=5&bool=true&string=hello');
// { num: 5, bool: true, string: 'hello' }

Is there a way to construct a return type in TypeScript such that it correctly types each value in the output? For the example above

{ num: number, bool: boolean, string: string }

Right now I'm just doing Record<string, string | boolean | number>, which doesn't really help once consuming the function (have to cast each individual value since I "know" what it should be).

Upvotes: 1

Views: 85

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074058

I don't think you can, no, and if you don't know what the string has in it when you call parseQueryParams, there's really not much of anything you can do about it. Your Record<string, string | boolean | number> or perhaps a Map, etc., is about the best you can do.

If you sometimes do know the shape (if not specific values) of the query string in advance, you can make the function generic so you can specify its return type in a type argument, which is basically a type assertion. This is one of those boundaries (like client/server) where when we cross it we have to specify what's going on.

That looks like this:

const parseQueryParams = <T extends object>(searchString: string) => {
  //                     ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
  const params = new URLSearchParams(searchString).entries();

  return [...params].reduce((out, [key, value]) => {
    // ...
  }) as T;
  // ^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
};

and then using it:

const result = parseQueryParams<{ num: number, bool: boolean, string: string }>('?num=5&bool=true&string=hello');
//                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

That way you're only doing it once and then you're fully-typed from there on out.

But again, that's only if you know the shape of the query string in advance.

You can make that type argument optional if you like, so you cater to both situations:

const parseQueryParams = <T extends object = Record<string, string | boolean | number>>(searchString: string) => {
    // ...
};

Upvotes: 1

Related Questions