🔍 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
- What do you want to use this for?
- Better constraints on function parameters without having to rely on brittle hacks.
- Better constraints on const assignment statements without having to rely on brittle hacks.
- More powerful string template types, without incurring in heavy CPU/memory costs.
- What shortcomings exist with current approaches?
- Some minor inconsistencies with capabilities associated to other types (such arrays)
- People tend to implement very slow types to work around the current shortcomings.
- Some very easy to define constraints are very difficult (or cumbersome) to implement.
- Or impossible to implement, so we can't have code as safe as we'd like.
- What workarounds are you using in the meantime?
- Forced type coercions
- Redundant runtime checks that affect application performance (because we can't statically know what we'll be passed to the function with enough precision).
- Much less refined types that accept values statically known to be invalid.
🔍 Search Terms
✅ Viability Checklist
⭐ Suggestion
I think it would be nice if TypeScript was able to narrow down the type for
lengthproperty of const string literals.Right now, we have the following:
What I'd expect is something like:
Some details to take into account:
stringvalues are immutable, leaving aside tons of potential interactions with "intrinsics", it should be easier to implement than narrowed-downlength's type forreadonlyarrays (from my probably naive perspective).constvalues. Althoughstringis immutable, a variable declared withletorvarcould 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":
📃 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):
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:
The known alternatives are:
as. This option adds extra work, and is too brittle.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 :
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:
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:
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