Skip to content
New issue

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

By clicking “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? Sign in to your account

Literal types and Arrray.prototype.includes typings #32201

Closed
5 tasks done
ethanresnick opened this issue Jul 1, 2019 · 4 comments
Closed
5 tasks done

Literal types and Arrray.prototype.includes typings #32201

ethanresnick opened this issue Jul 1, 2019 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@ethanresnick
Copy link
Contributor

Search Terms

includes (in the label "Domain: lib.d.ts")

Suggestion

Change the typing for A.p.includes in lib.d.ts. See below.

Use Cases/Examples

I often use Array.prototype.includes to validate at runtime that a user-provided value is in some list of legal values:

declare const userValue: string; // not any, because already validated as string
const legalValues = ["a", "b", "c"];

if(!legalValues.includes(userValue)) {
  throw new Error("...");
}

The above works great. However, the problem comes when I want to use literal types for my legal values. In my code, I want to do that so I can make sure I define a "handler" for every legal value:

const legalValues = <const>["a", "b", "c"];

// later on... 
// Because legalValues entries are literal types, 
// I get a compiler error if I forget to define any behaviors
const behaviors: { [K in typeof legalValues[number]]: any } = {
  a: something,
  b: somethingElse,
  c: anotherThing
};

The problem is that, with the literal types, the includes call now gives a type error:

// Error: Argument of type number is not assignable to 1 | 2 | 3
if(!legalValues.includes(userValue)) {
  throw new Error("...");
}

I think a more natural/appropriate behavior for type signature of includes would be for the literal types to be widened. But, since I don't think there's a way to write that in the type signature directly, I'd propose something similar manually by changing the definition from:

includes(searchElement: T, fromIndex?: number): boolean;

to:

includes(searchElement: T extends string ? string : (T extends number ? number : T), fromIndex?: number): boolean;

Checklist

My suggestion meets these guidelines:

  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@jcalz
Copy link
Contributor

jcalz commented Jul 1, 2019

Related to or duplicate of #26255

Also relevant Stack Overflow answer

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jul 1, 2019
@ethanresnick
Copy link
Contributor Author

Thanks for the reference, @jcalz. I'm not surprised that this has come up before. I'd like to think this issue isn't quite a duplicate, in that it offers a proposal for fixing the problem in the standard lib without waiting for lower-bounded type parameters to be implemented in TS.

That said, I'd like to update my proposal to be more general, following your answer on Stack Overflow and hack mentioned at the top of #14520. So, that would be:

includes<U extends (T extends U ? unknown : never)>(searchElement: U, fromIndex?: number): boolean;

or:

includes<T extends U, U>(searchElement: U, fromIndex?: number): boolean;

@RyanCavanaugh How do you feel about updating the standard lib to use one of these known workarounds while we're waiting for upper bounded generics? Personally, I don't see any harm, especially if the first workaround given above (from @jcalz's Stack Overflow answer) is used, as the final signature would likely be backwards compatible with that workaround, I think (since both would introduce one type parameter that means conceptually the same thing).

@typescript-bot
Copy link
Collaborator

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

@cefn
Copy link

cefn commented Mar 1, 2021

I have been experimenting with this implementation, which seems to resolve the issue for me and makes no casts, ensuring that Typescript can keep doing its good job of widening as much as necessary and narrowing as much as possible without me having to claim I know better than the compiler. I'm posting it here since it doesn't seem to have been mentioned anywhere.

function constArrayIncludes<Item extends any, Arr extends readonly Item[]>(
  arr: Arr,
  item: Item
) {
  return arr.includes(item);
}

It ensures I can use e.g. ...

const operatorNames = ["add", "subtract", "divide", "multiply"] as const;

...to define valid literals available to javascript, then write typeguards like...

function operatorGuard(tile:Tile) : tile is Operator{
    return constArrayIncludes(operatorNames, tile);
}

There also seems to be a useful Generic implementation using the same strategy...

function genericGuard<Item extends any, Arr extends readonly Item[]>(arr:Arr, item:Item): item is typeof arr[number]{
    return constArrayIncludes(arr, item);
}

The full experiment, including a Generic typeguard for cases of const arrays is here in this playground

As shown in this screenshot it also correctly reports a failure against a typical problem case that @RyanCavanaugh mentioned in #26255

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants