Home > Back-end >  How to narrow the type of the value returned by URLSearchParams.get() to union type?
How to narrow the type of the value returned by URLSearchParams.get() to union type?

Time:03-02

There is a search parameter in URL like this: ?mode=view. The valid value of mode should be 'edit' and 'view', so I create a ModeTuple type for it and convert it to union type using indexed access types ModeTuple[number].

type ModeTuple = ['view', 'edit'];

const searchParams = new URLSearchParams();
const modes: ModeTuple = ['view', 'edit'];

const m1: ModeTuple[number] = modes.includes(searchParams.get('mode')) ? searchParams.get('mode') : 'view';

I want to check if the value of mode is valid using modes.includes(...), then narrow the type to union type 'edit' | 'view'.

Got error:

Argument of type 'string' is not assignable to parameter of type '"view" | "edit"'

It seems Array.prototype.includes method will NOT narrow the type from string to "view" | "edit" successfully. From the logic of the JS code, the mode value must be 'view' or 'edit'. But TSC doesn't know that.

TypeScript Playground

CodePudding user response:

const m1: ModeTuple[number] = modes.includes(searchParams.get('mode')) ? searchParams.get('mode') : 'view'; won't work for two reasons:

  1. mode.includes expects a ModeTuple[number] argument, but searchParams.get('mode') returns string | null.

  2. TypeScript's flow analysis for narrowing types is limited. Even if #1 weren't an issue, it wouldn't know from modes.includes(searchParams.get('mode')) that a subsequent call to searchParams.get('mode') will return a ModeTuple[number] value. (Not least because not all functions are pure, so TypeScript can't assume in the general case that a function will return the same thing twice when called twice with the same arguments.)

For things like this, I like to start with a constant array, then derive the types from it:

const modes = ["view", "edit"] as const;
type ModeTuple = typeof modes; // If you want it
type Mode = ModeTuple[number];

That way, the values are only listed once and I only have one place to change them if I need to add one, etc.

Then, you could replace your original m1 code with:

const m1 =
    modeString !== null && (modes as readonly string[]).includes(modeString)
    ? modeString as ModeTuple[number]
    : "view";

...but I don't like doing inline type assertions like that, because they're verbose and repeating them gives me multiple chances to get it wrong.

Instead, I prefer defining a single type predicate (aka "type guard") function that I can reuse:

function isMode(mode: string | null): mode is Mode {
    return mode !== null && (modes as readonly string[]).includes(mode);
}

Then you could do:

const modeString = new URLSearchParams(location.search).get("mode");
const m1 = isMode(modeString) ? modeString : "view";

Or you could define a function to do it, if you may need to do it in more than one place:

function getMode(searchParams: URLSearchParams): Mode | null {
    const mode = searchParams.get("mode");
    // Or if you didn't define `isMode`, just do the check here
    return isMode(mode) ? mode : null;
}

Then getting it is just:

const m1 = getMode(theSearchParams):

Playground link

  • Related