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

Feature Request: narrow down type for length property of string literals & allow length constraints on func params #59615

Closed
6 tasks done
castarco opened this issue Aug 13, 2024 · 3 comments

Comments

@castarco
Copy link

castarco commented Aug 13, 2024

πŸ” Search Terms

  • narrow down
  • const string length

βœ… Viability Checklist

⭐ Suggestion

I think it would be nice if TypeScript was able to narrow down the type for length property of const string literals.

Right now, we have the following:

const myString = 'hello' // `myString`'s type is 'hello', we statically know its length
type MyStringLength = (typeof myString)['length'] // === number, but length info is lost

What I'd expect is something like:

// string values are immutable, so their length does not change, but we still
// need the value to be declared as a constant so we can "bind" the length's
// narrowed down type to the `myString` reference.
const myString = 'hello'
type MyStringLength = (typeof myString)['length'] // === 5
const myLength = myString.length // `myLength`'s type is 5

// The former wouldn't be relevant if it wasn't because we want to use it to
// constraint function parameters
function f(s: string & { length: 5 }) {
  // do stuff
}

f('hello') // `f` accepts 'hello'

Some details to take into account:

  • string values are immutable, leaving aside tons of potential interactions with "intrinsics", it should be easier to implement than narrowed-down length's type for readonly arrays (from my probably naive perspective).
  • This should be ONLY for const values. Although string is immutable, a variable declared with let or var could change where is pointing to, therefore invalidating the type-inferred length.

Some "aesthetic" reasons for wanting this feature that don't fit as "motivating example" nor "use case":

  • It improves "consistency" with what we already have for readonly arrays.

πŸ“ƒ Motivating Example

Case 1: Constrained Assignments & Function Parameters

While we can create type guard functions to ensure the length of a string, relying on much simpler conditional type guards does not work well. This is an entirely different problem than the one I'm pointing at, but still relevant, listed here for convenience (wait for the second part of this case, where I reach the point I really want to mention):

let s = 'hello' // 

// Simple conditional type guard does not work
if (s.length === 5) {
  let ll = s.length // `ll`'s type === number
}

// With extra effort (and runtime perf penalty), we can have a proper type guard
function ensureLength<const L extends number>(
  s: string,
  l: L
): s is (string & { readonly length: L }) {
  return s.length === l
}

if (ensureLength(s, 5)) {
  let ll = s.length // `ll`'s type === 5
}

As I was saying, we can write function type guards... but they only let us set types for "outputs", assignments are a completely different beast:

// @ts-expect-error : it fails, although we statically know the length
const myString: string & { length: 5 } = 'hello'

// @ts-expect-error : it fails, although we statically know the length
const myString2: string & { readonly length: 5 } = 'hello'

// ------

// The same happens with function parameters, which is usually more relevant
// than const assignments.
type Str5 = string & { length: 5 }

function f(s: Str5) {
  // do stuff
}

// @ts-expect-error : it fails, although we statically know the length
f('hello')

The known alternatives are:

  • Force types with as. This option adds extra work, and is too brittle.
  • Extra runtime checks. This introduces unnecessary performance penalties for things that we already know at compile time.

Case 2: More powerful Template Literals without RegExp nor combinatory explosions

There are many open issues asking for regular expressions or similar mechanisms embedded into template literals. There are also sound reasons to not rush any feature in that direction :

  • JavaScript's regular expressions are not really "pure" regular expressions, they provide many interesting features (lookahead, lookbehind, their negative counterparts...) that could be used to blow up compilation times or even perform DoS attacks with a simple PR.
  • Even "pure" regular expressions could be abused, not so easily, but it's possible.
  • TypeScript team can't afford having its own RegExp "safe" subset just for this feature, the maintenance cost would be too high.

On the other hand, and in the absence of anything resembling regular expressions for string templates, many people have tried going through more "hacky" paths. The basic idea is simple, but it fails miserably. Let's use UUIDs as an hexample:

type Hex = '0' | '1' | '2' | '...' | 'e' | 'f'
type Hex4 = `${Hex}${Hex}${Hex}${Hex}` // This one is already blowing up
type Hex8 = `${Hex4}${Hex4}` // worse
type Hex12 = `${Hex4}${Hex4}${Hex4}` // even worse

// The compiler died long before reaching this point, this last one
// is like Thanos killing half of the Universe.
export type UUID = `${Hex8}-${Hex4}-${Hex4}-${Hex4}-${Hex12}`

Although it wouldn't be as powerful as having character subsets nor regular expressions, being able to force a specific length for "open ended" string template types would make it possible to introduce tons of "cheap" type refinements:

type Str4 = string & { readonly length: 4 }
type Str8 = string & { readonly length: 8 }
type Str12 = string & { readonly length: 12 }

// Given that Str4, Str8 and Str12 are not union types, we should be able
// to define our UUIDish type without falling into a combinatory explosion.
type UUIDish = `${Str8}-${Str4}-${Str4}-${Str4}-${Str12}`

This is not the only alternative, though. I really don't know enough about how string template types are implemented, but one (very theoretical, and probably wrong) possibility would be to avoid computing the whole extension of the type, and only checking for type matches instance by instance (or to provide a "manual" way to avoid computing the extension of the type, in case doing that for all cases introduced regressions of any kind).

πŸ’» Use Cases

  1. What do you want to use this for?
    1. Better constraints on function parameters without having to rely on brittle hacks.
    2. Better constraints on const assignment statements without having to rely on brittle hacks.
    3. More powerful string template types, without incurring in heavy CPU/memory costs.
  2. What shortcomings exist with current approaches?
    1. Some minor inconsistencies with capabilities associated to other types (such arrays)
    2. People tend to implement very slow types to work around the current shortcomings.
    3. Some very easy to define constraints are very difficult (or cumbersome) to implement.
    4. Or impossible to implement, so we can't have code as safe as we'd like.
  3. What workarounds are you using in the meantime?
    1. Forced type coercions
    2. Redundant runtime checks that affect application performance (because we can't statically know what we'll be passed to the function with enough precision).
    3. Much less refined types that accept values statically known to be invalid.
@MartinJohns
Copy link
Contributor

Duplicate of #34692.

@castarco
Copy link
Author

castarco commented Aug 13, 2024

@MartinJohns thank you for pointing that one out. There's one detail, though, that makes me wonder if it's exactly the same.

  • I focus explicitly on const-declared values for some reasons I list in this same issue, while the other issue does not make any distinction.
  • I also focus on the ability to define userland types that allow to improve type constrains on const literal string values, while the other issue completely disregards that aspect (the only reference I found jumps directly into proposing new intrinsic types, which is against TypeScript guidelines).

@castarco castarco changed the title Feature Request: narrow down type for length property of string literals Feature Request: narrow down type for length property of string literals & allow length constraints on func params Aug 13, 2024
@castarco
Copy link
Author

Well, I'll close it anyway. I'll leave a reference to my comments there.

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

No branches or pull requests

2 participants