microsoft / TypeScript

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

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

Closed castarco closed 2 months ago

castarco commented 2 months ago

πŸ” Search Terms

βœ… 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:


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

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:

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:

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 commented 2 months ago

Duplicate of #34692.

castarco commented 2 months ago

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

castarco commented 2 months ago

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