Skip to content

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

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

Open
6 tasks done
Akindin opened this issue Nov 8, 2024 · 2 comments
Open
6 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@Akindin
Copy link

Akindin commented Nov 8, 2024

πŸ” 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:

  • Improved Type Inference: Developers will get precise types when concatenating string literals.
  • Better Code Completion and Error Detection: IDEs can provide better suggestions and catch more errors at compile-time.
  • Consistency: Aligns with TypeScript's goal of providing accurate and useful type information.

Potential Drawbacks:

  • Complexity: This might increase the complexity of TypeScript's type system for string operations.
  • Performance: There could be an impact on type-checking performance for very complex string concatenations.
  • Error on reassign There could be a problems when concat used in let variable initialization
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, ''>}`;
    toString<This extends string>(this: This): This;
    toUpperCase<This extends string>(this: This): Uppercase<This>;
    toLowerCase<This extends string>(this: This): Lowercase<This>;
    valueOf<This extends string>(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


πŸ“ƒ 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:

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 RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Nov 8, 2024
@RyanCavanaugh
Copy link
Member

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
Copy link
Author

Akindin commented Nov 8, 2024

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants