Description
π Search Terms
ReadonlyArray includes type-guard
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β Suggestion
Add an overload to Array.prototype.includes
and ReadonlyArray.prototype.includes
that narrows the type of the searched value when the array is a literal tuple (e.g., declared with as const
) containing only primitive literal values such as strings, numbers, booleans, null
, or undefined
.
This overload enables stricter type checking for includes
calls and allows using includes
as a type guard only when it is safe and meaningful to do so, avoiding false assumptions when working with general arrays.
interface ReadonlyArray<T> {
includes<U>(
s: U,
...args: IsLiteralArray<this> extends true
? T extends U
? [fromIndex?: number]
: never
: never
): s is IsLiteralArray<this> extends true ? (T extends U ? T : never) : never;
includes<U>(
s: U,
...args: T extends U ? [fromIndex?: number] : never
): boolean;
}
interface Array<T> {
includes<U>(
s: U,
...args: T extends U ? [fromIndex?: number] : never
): T extends U ? boolean : false;
}
Supporting Utility Types:
type IsUnion<T> = (T extends T ? (p: T) => 0 : never) extends (p: T) => 0
? false
: true;
type IsLiteral<T> = IsUnion<T> extends true
? false
: T extends null | undefined | boolean
? true
: T extends PropertyKey
? {} extends Record<T, unknown>
? false
: true
: T extends bigint
? {} extends Record<`${T}`, unknown>
? false
: true
: false;
type IsLiteralArray<T extends readonly unknown[]> = IsUnion<T> extends true
? false
: IsLiteral<T['length']> extends true
? T extends readonly [infer F, ...infer R]
? IsLiteral<F> extends true
? IsLiteralArray<R>
: false
: true
: false;
You can try it in the TypeScript Playground here
π Motivating Example
Currently, code like the following produces a type error, even though it is semantically correct:
const KEYS = ['aaa', 'bbb', 'ccc'] as const;
function handleKey(key: string) {
if (KEYS.includes(key)) {
// This should narrow key to 'aaa' | 'bbb' | 'ccc'
doSomething(key);
}
}
To avoid this error, users often resort to unsafe casts or unnecessary widening of the array's type. On the other hand, relaxing the parameter type (e.g., using unknown
) can lead to incorrect code such as:
const KEYS = ['aaa', 'bbb', 'ccc'] as const;
function handleKey(key: number) {
if (KEYS.includes(key)) {
// This is likely a bug, but it won't be caught
}
}
The proposed overload solves this by enabling proper narrowing only when it is safe (i.e., when the array is a literal tuple of literals), and producing compile-time errors when there is no possible match.
π» Use Cases
- What do you want to use this for?
- What shortcomings exist with current approaches?
- What workarounds are you using in the meantime?
1. What do you want to use this for?
To safely narrow the type of a variable when checking its inclusion in a fixed set of literal values using .includes
, especially in configuration validation, value guards, and API parameter checks.
2. What shortcomings exist with current approaches?
includes
does not currently act as a type guard even when used with literal tuples.- It requires manual guards or unsafe type assertions.
- Overly permissive typing (e.g., accepting
unknown
) eliminates type safety and allows obvious mistakes.
3. What workarounds are you using in the meantime?
- Writing custom type guards (
key is 'a' | 'b' | 'c'
) - Using
Set.has()
instead of arrays - Using type assertions like
as any
- Duplicating union types and literal arrays