Skip to content

Smarter String includes/endsWith/startsWith using template literal type predicates #58152

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

Closed
btoo opened this issue Apr 11, 2024 · 3 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@btoo
Copy link

btoo commented Apr 11, 2024

⚙ Compilation target

ES2015

⚙ Library

ES2015.Core

Missing / Incorrect Definition

     /**
      * Returns true if searchString appears as a substring of the result of converting this
      * object to a String, at one or more positions that are
      * greater than or equal to position; otherwise, returns false.
      * @param searchString search string
      * @param position If position is undefined, 0 is assumed, so as to search all of the String.
      */
-    includes(searchString: string, position?: number): boolean;
+    includes<SearchString extends string>(searchString: SearchString, position?: number): this is `${string}${SearchString}${string}`
     /**
      * Returns true if the sequence of elements of searchString converted to a String is the
      * same as the corresponding elements of this object (converted to a String) starting at
      * endPosition – length(this). Otherwise returns false.
      */
-    endsWith(searchString: string, endPosition?: number): boolean;
+    endsWith<SearchString extends string>(searchString: SearchString): this is `${string}${SearchString}`
+    endsWith<SearchString extends string>(searchString: SearchString, endPosition: number): this is `${string}${SearchString}${string}`
     /**
      * Returns true if the sequence of elements of searchString converted to a String is the
      * same as the corresponding elements of this object (converted to a String) starting at
      * position. Otherwise returns false.
      */
-    startsWith(searchString: string, position?: number): boolean;
+    startsWith<SearchString extends string>(searchString: SearchString): this is `${SearchString}${string}`
+    startsWith<SearchString extends string>(searchString: SearchString, position: number): this is `${string}${SearchString}${string}`

Sample Code

declare const s: string

const searchString = 'searchString'

if (s.includes(searchString)) {
  s
  // ^? const s: `${string}searchString${string}`
}

if (s.endsWith(searchString)) {
  s
  // ^? const s: `${string}searchString`
}

if (s.endsWith(searchString, 0)) {
  s
  // ^? const s: `${string}searchString${string}`
}

if (s.startsWith(searchString)) {
  s
  // ^? const s: `searchString${string}`
}

if (s.startsWith(searchString, 0)) {
  s
  // ^? const s: `${string}searchString${string}`
}

playground link

Documentation Link

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith

@MartinJohns
Copy link
Contributor

Duplicate of #46958 (and others).

@btoo
Copy link
Author

btoo commented Apr 11, 2024

w.r.t. #46958 (comment)

Changing a function from not-overloaded to overloaded has fairly pervasive effects in areas like contextual typing, generic inference, and how exactly it can be invoked when the method operand is a union

fwiw the includes here still isn't an overload

I believe the startsWith/endsWith need not be either, if that's a blocker, e.g.

    startsWith<SearchString extends string, Position extends number | undefined = undefined>(searchString: SearchString, position?: Position):
      this is Position extends number
        ? `${string}${SearchString}${string}`
        : `${SearchString}${string}`

playground link

template string literals are inherently combinatorially explosive, so a call like startsWith(union of 24) && endsWith(union of 24) produces a union of 576 elements, which is bad from a performance and comprehensibility perspective

this is a legitimate concern, but is it more of an issue with template literal types than the implementation of String methods? i suppose something can be said about encouraging/discouraging these combinatorial explosions though..

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Apr 11, 2024
@typescript-bot
Copy link
Collaborator

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

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Apr 14, 2024
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

4 participants