Typescript: Support ReadonlyArray.includes as a type guard

Created on 18 Apr 2019  ·  7Comments  ·  Source: microsoft/TypeScript

Search Terms

  • ReadonlyArray type guard

Suggestion

For cases where ReadonlyArray's type argument is a subtype of string or some other type handled by as const, it’s useful to be able to do:

const VALID_VALUES = ['a' | 'b'] as const;
function doStuffIfValid(x: string) {
    if (VALID_VALUES.includes(x)) {
        // $ExpectType 'a' | 'b'
        x
    }
}

Note that the above produces an error on line 3 about string not being assignable to 'a' | 'b' in the VALID_VALUES.includes(x) call.

Use Cases

I want to use this to improve type guards within code that interacts with untyped third‑party JavaScript.

Examples

some-module/index.ts

export type ValidValues = 'a' | 'b';
const VALID_VALUES = ['a' | 'b'] as const;

export function doStuffIfValid(thing: Record<string, string>, x: ValidValues): void;
export function doStuffIfValid(thing: Record<ValidValues, Record<string, string>>): void;

export function doStuffIfValid(thing: unknown, x?: unknown) {
    if (typeof x === 'string') {
        if (!VALID_VALUES.includes(x)) {
            throw new TypeError(`x must be one of ${VALID_VALUES} or undefined, got ${x}`);
        }
        // Do something where x is 'a' | 'b'
    } else if (typeof x !== 'undefined') {
        throw new TypeError(`x must be one of ${VALID_VALUES} or undefined, got ${x}`);
    }
}

Right now, VALID_VALUES has to be expressed as:

const VALID_VALUES = ['a' | 'b'] as unknown as TypeGuardReadonlyArray<ValidValues>;
interface TypeGuardReadonlyArray<T> extends ReadonlyArray<T> {
    includes(searchElement: unknown, fromIndex?: number): searchElement is T;
}

for the above to not cause compilation errors and the type guard to work properly.

Note that this only works correctly when T is a literal union type.

untyped-third-party-code.js

const stuff = require('some-module');

// Doesn't throw
stuff.doStuffIfValid({}, 'a');
stuff.doStuffIfValid({}, 'b');
stuff.doStuffIfValid({a: {}, b: {}});

// Throws during runtime, as this file is untyped third-party code.
stuff.doStuffIfValid({}, 'c');

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Working as Intended

Most helpful comment

Try as readonly string[], or if that fails, .includes(s as any).

All 7 comments

Without #15048, this is a problematic change. Consider:

const someStrings: ReadonlyArray<string> = ["a", "b", "c"];

function fn(x: number | string) {
  if (someStrings.includes(x)) {
    //
  } else {
    // here, x is assumed to be 'number', but that's wrong!
  }
}
fn("d");

That’s why I only want this to apply when T is a literal union type (i.e. the result of as const).

That has the same problem though. A type is always a bound on a value, not an exact specification

const someStrings: ReadonlyArray<"a" | "b" | "c"> = ["a"];

function fn(x: "b" | "x") {
  if (someStrings.includes(x)) {
    //
  } else {
    // here, x is wrongly assumed to be "x"
  }
}
fn("b");

Yeah, but you probably shouldn't be doing const someStrings: ReadonlyArray<"a" | "b" | "c"> = ["a"];, which is why I suggested this.

My use case is const someStrings = ["a"] as const.

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

I encountered the same case.
Here is my workaround:

const OS_TYPE = ['Linux', 'macOS', 'Windows'] as const
type OSType = typeof OS_TYPE[number]

//user defined type guard
function isOSType(s: string):s is OSType{
    if ((OS_TYPE as any as string[]).includes(s))
        return true;
    return false;
}

as any as string[] seems like a poor idea? Is there a better way?

Try as readonly string[], or if that fails, .includes(s as any).

Was this page helpful?
0 / 5 - 0 ratings