Skip to content

Proposal: Add Overloads to Array.prototype.includes and ReadonlyArray.prototype.includes for Improved Type Safety and Usability #61618

New issue

Have a question about this project? No Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “No Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? No Sign in to your account

Open
5 of 6 tasks
sugoroku-y opened this issue Apr 26, 2025 · 2 comments

Comments

@sugoroku-y
Copy link

sugoroku-y commented Apr 26, 2025

πŸ” Search Terms

ReadonlyArray includes type-guard

βœ… Viability Checklist

⭐ 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

  1. What do you want to use this for?
  2. What shortcomings exist with current approaches?
  3. 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
@MartinJohns
Copy link
Contributor

You mistakingly checked this point:

This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types

@sugoroku-y
Copy link
Author

sugoroku-y commented Apr 26, 2025

You mistakingly checked this point:

This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types

Sorry.

The type function is only to determine if it is a literal fixed array, so I did not intend to add that as a utility type, but if it is used in a global type, I would have to add it.

I unchecked it.

Thank you.

No Sign up for free to join this conversation on GitHub. Already have an account? No Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants