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.
I want to use this to improve type guards within code that interacts with untyped third‑party JavaScript.
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}`);
}
}
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.
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');
My suggestion meets these guidelines:
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).
Most helpful comment
Try
as readonly string[], or if that fails,.includes(s as any).