microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.17k stars 12.51k forks source link

Proposal: Enhance String interface definition to support type inference for string literals #60456

Open Akindin opened 2 weeks ago

Akindin commented 2 weeks ago

šŸ” Search Terms

string generic methods, string concat, checked domain literal types

Related issues #44268

āœ… Viability Checklist

ā­ Suggestion

Current Behavior: Currently, any manipulation on string other then assigning to literals and using ${templates} doesn't infer the type from literal string. Basic toString and valueOf will lose type of literal string in the process. Desired Behavior: I propose enhancing the type definitions for String interface so that it can infer the exact type when used with string literals and templates. This would improve type safety and developer experience when working with string manipulation. At least valueOf, toString, toUpperCase, toLowerCase can be implemented without changing something other than the definition of String interface.

Example of Current Issue:

const result = 'hello'.concat(' ', 'world'); // TypeScript infers 'string' instead of 'helloworld'

Proposed Solution:

Introduce a new type definition for concat that uses variadic tuple types to infer the correct concatenated string literal type:

type Join<S extends string[], D extends string> = 
    S extends [] ? '' :
    S extends [infer First extends string, ...infer Rest extends string[]] ? 
        `${First}${Rest extends [] ? '' : D}${Join<Rest, D>}` : string;

interface String {
    concat<This extends string, S extends string[]>(this: This, ...strings: S): `${This}${Join<S, ''>}`;
}

const c = 'qwery'.concat("123", 'abcd')

Benefits:

Potential Drawbacks:

interface String { concat<This extends string, S extends string[]>(this: This, ...strings: S): ${This}${Join<S, ''>}; toString(this: This): This; toUpperCase(this: This): Uppercase; toLowerCase(this: This): Lowercase; valueOf(this: This): This; }

let a = "123".concat("qwerty");

a = "something else"; // this will result in error if implemented as interface modification because of inferred type "123qwerty"


**Additional Context:**
This change would particularly benefit scenarios where string templates or literal string concatenation are heavily used, enhancing the robustness of TypeScript's type system in string manipulation contexts.

[Playground](https://www.typescriptlang.org/play/?#code/C4TwDgpgBAUg9gSwHYB4DKUIA9gSQEwGcpDgAnZAcwG0BdAGigBFMc8iTyqA+KAXigAoKCKgZsuAsTpQA-FADkCqAC5ho8WylRqyAGYQyUAGIIypVpI6kKSSowB0T-YagAlCBYntiNqnVo5IVEQqAADABIAb1NzYABfaI8vLQ4ZeSVVZkSo+GQUZOBGJm54sKy-OwBuQUFQSDEAGwQAYwg0YABDMmB0Sx9OW3soADl+7SQAVwBbACNDRgBBcesuOxkBOl4BdRFIqMXqBUa8SmAACwVaMpXifZGb3dD5NCeQlTFb8OiXI1jSHK-dyeBLlN6hEQvZptDrdXqFRgjRjUf5FKBOByLWi8cEQj5oGp1cDQNDQiAAUQIfW82kqw0p+C+UzmCygyxpHE6SBAGx0DDZLRaXzp-EUCm2wVE+0Ox1OFyuNw5d2iDLKuKCi0F6vxX32QNRgKQBiMhTVEIhUNaFKp6vNogRtrtUAZ9EdduoGMWjFRDDd5ulgpyBrCfpE3D9H01LUJ9WgAGFzt1Fr1NFZfGthgBJJkzeZkJbCjO8rai9SpgZ6o2uA0-KsmkFm0TyGUnOzywJKqCZ8HyVHgj4JpPwkGMTPIz3esykbFPD4AIjnMeJoxmhlaAAU4IQEMAEAA3CAoAAqy87zLzEv2J8gitSdwAtNFz4YbvIkBAD0YPteIFURLVkFwMg9E6NoxAzKAoieFo4CQFpOl6I9zgQYhOzpRhy1pItsQACguFDv2QwhHCcOlCHxABKD4ryInI8lQNBGCUUowhqEIWkTMhk2PIjCyGRhBzITdULvKBnzIbg8KIwiUMYDjumEgdOOEqioEE7ikNktTlK3bg2NEBCeJQviqAEnSRLTMTc0MST8PIqBNOIqB5KErclIUrdVPUxCiLMjzCD0p5gDgDohiMiyBjpWzpIcojVMc-SRGCgBVMBIDIONOkIQ9HJMuxooI2KUNU1L0vg7LwsCkJgoAGTgAB3QxMoq3K0IzAr7Mc1S6sashypyoiqtEPdOkaSYIAAeT0cK8soDqZMIeKiMSkgyRmtr+PAuEcxZfNnQIHa81FABGCAAE4LvmoqnNIOF8S6HpGHYWQPgZVTSStWEenQMkGXCxgGW4DCHuAcN4lqE5gGc0UFAAR16kAFAcGC4IQnC52OgAmABmOcmMWAAhOMmAUCjCRRix6oAfRc0VYzgPRnORzjkxw46ycECmoYgGnONFFoHEIMkcMxxhsY5rnMGmMBQHnOd+eZoc2YABmViXYIsGCwBAY6FeC0KqBw9WkE1uBtcxhWRrGya9CNmpJcmNLXAEAWUqdjKsogI3OY1qHGga52mdqgOPey72fZNqG9A4AR33q8ChnRgAWXGKIcfWM3DyXOlFOcAB1JiYAAOAB2ckC6YONlaTucHDRtWI8IOATgcf3KBwzoKIhiBubIMg4CMAQ5xTzHa5R+DgHR465y7wRDH7weoDnJvph75C7CpiBGmyxdakbqOEC3mOdDnJBOlXvGl4QfBL7nGDSDnQIsuc33CT0SY4N3WCoEoPBDAQ9oA9dx2AAIrjTICASk5AQAoFMEfWajAJpkHwK4Tsc5FhoDjPLAAPkvJg5JMFzkknoQ+jRGQfDgWQxgA8UFkA+Eg2hFFIJPDID3SYZAkBQBIUfRWXFJ5q3TnAUqTVPZG2RrBCeOFuFkMFsLdmjA5x8EvjQwwHNwb7ygLgCwAhpFEAcNMToYAcJSNIfgJhfBeC6PEajSec4RjnwgDPMmUAAD0LioC-GILGDRWjgAWx0aYwg+jDHGN0eY3g+xdHxHsavMIzi3FQHwHATwSAFBQyBBcaA3iNGsNhuNbRP8-5kAAWgIBVAwGGEgUgaBJij6EGoMrfk6DCFdyAA)

---

### šŸ“ƒ Motivating Example

In TypeScript, while working with string literals, certain operations like concatenation or transformations (e.g., toUpperCase, toLowerCase) typically result in the loss of specific literal types, being inferred as a general string. This can lead to a loss of valuable type information, resulting in less strict compile-time checks and the need for manual type assertions or annotations.

Consider the following example:

```typescript

const basePath = "/api";
const usersPath = "/users";
const fullPath = basePath.concat(usersPath);  // Inferred as `string`

Here, despite knowing that basePath is "/api" and usersPath is "/users", TypeScript loses the literal type information after concatenation, inferring fullPath as string, rather than "/api/users". This loss of precision means we can't rely on TypeScript to enforce strict types when building paths or identifiers, leading to potential runtime errors.

šŸ’» Use Cases

  1. What do you want to use this for? To get rid of boilerplate when transforming strings and ensure that result of transformation satisfies the constraints
  2. What shortcomings exist with current approaches? Explicit type declaration and cast after manipulations
  3. What workarounds are you using in the meantime? Create a bunch of utils functions that work as a Proxy for calling built in methods
RyanCavanaugh commented 2 weeks ago

I don't really understand the value, to be honest

Case 1: The input to concat is a string. In this situation, nothing changes

Case 2: The input to concat is a single literal. In this situation, why not just use a template literal?

Case 3: The input to concat is a union. In this situation, a combinatorial explosion is very likely, which is bad.

Akindin commented 2 weeks ago

In case of general string type my proposal doesn't aim to change this, it focuses on preserving literal types when possible. This is about enhancing the handling of literal strings, not general strings.

Also with concat you can do feats like this with type inference

const fields = ["name", "id", "cost"] as const;
const test3 = "123".concat(...fields);

As for now template literals can't infer the string literal type unless explicitly specified.

function generateURL<T extends string>(value: T) {
    return "https://api.example.com/".concat(value);
}

function generateURLTemplate<T extends string>(value: T) {
    return `https://api.example.com/${value}`;
}

const userURL = generateURL('user'); // infers the type
const userURL2 = generateURLTemplate('user'); // becomes just string

About combinatorial explosion this issue does exist with template literals and there is an interesting proposal Lazy evaluation of template literals. For autocomplete purposes it can show the next possible literal in a sequence, instead of trying to generate all the variants. With lazy evaluation it is possible to check robust config in compile time, for something like date format validation or language restrictions to reduce the probability of out of ASCII rande identical symbols to pass (highlighting helps, but not with mixed text where you can have one fields that can have everything and another only ASCII symbols).