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

Enhance members of literal type like const array type (e.g. string literal types should have literal length property) #34692

Open
samchon opened this issue Oct 24, 2019 · 13 comments
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

@samchon
Copy link

samchon commented Oct 24, 2019

The const array type, its principle members are also be the constant. When define a const array [number, string, boolean], its length type be 3. Also, when access to special index, the type also points the exact const type.

function const_array(): void
{
    let elements: [number, string, false] = [3, "something", false];
    let length: 3 = elements.length;

    let first: number = elements[0];
    let second: string = elements[1];
    let third: false = elements[2];
}

However, the literal type is not like the const array. When define a literal type "something", its length be not 9 but number. Also, when access to a special index, the type is not a special literal type but a string type.

function literal(): void
{
    let word: "something" = "something" as const;
    let length: number = something.length; // not 9 but number

    let first: string = word[0]; // not "s" but string
    let second: string = word[1]; // not "o" but string
    let third: string = word[2]; // not "m" but string
}

What about enhancing the literal type to be like the const array type?

@MartinJohns
Copy link
Contributor

Sounds like a duplicate of #34589.

@fatcerberus
Copy link

No, I don't think so - #34589 is about tuple types. This is about using literal types for the .length and elements of a string literal.

@RyanCavanaugh RyanCavanaugh added 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 labels Oct 30, 2019
@RyanCavanaugh
Copy link
Member

Would be nice to understand why this would be useful

@samchon
Copy link
Author

samchon commented Oct 31, 2019

https://github.com/samchon/tgrid/blob/467aa3b8b619aab36bb50257feaa8de208de5888/src/components/Driver.ts#L85-L91

type is_edge_underscored<P> = P extends string
    ? P[0] extends "_"
        ? true
        : P[typeof (P.length - 1)] extends "_"
            ? true
            : false
    : false;

@RyanCavanaugh To implement a type detecting the edge-underscored string, I've also published a new issue #34844, hoping (Literal.length - 1) also be the constant value. However, I can't sure that I've requested exact features for the edge-underscored string. If I'm forgetting something important, please inform me.

@augustobmoura
Copy link

There is a lot of use cases

One use case is asserting that a method is only called for a single character, mimicking the char type from other languages:

type SingleChar = string & { length: 1 };
const charCodeOf = (char: SingleChar): number => char.charCodeAt(0);

charCodeOf('a'); // Should work
charCodeOf('asdhfkj') // Should fail
charCodeOf('') // Should also fail

Also prevent literal empty strings from being passed as arguments:

type EmptyString = string & { length: 0 };
type SingleChar = string & { length: 1 };

const lastChar = (str: Exclude<string, EmptyString>): SingleChar => str[str.length - 1]!;

lastChar('abc'); // Should work
lastChar(''); // Should fail

// We could also better narrow the return types

const typedLastChar = <S extends string>(
  str: S,
): S extends EmptyString ? undefined : SingleChar => str[str.length - 1];

const ch1: string = typedLastChar('qwe'); // should work
const ch2: string = typedLastChar(''); // should fail
const ch3: undefined = typedLastChar(''); // should work

I'm sure there's others cases to type against fixed length strings (fixed length hashs maybe? or identifications cards?)

In my opninion the proposal is a really good addition, it's amazing for typing DSLs and builders

@unional
Copy link
Contributor

unional commented Jan 22, 2021

Another use case: currently some type level arithmetics leverage the fact that array length produces number literal.
This creates a performance issue.
I am doing that in type-plus and during development I have to limit the digits to 3-4 so that the IDE is still performant.

With template literal type, if 'abc'['length'] gets the same treatment as [1,1,1]['length'], there might be a big performance gain there.

Another example of type level arithmetics: Implementing Arithmetic Within TypeScript’s Type System

But of course, the best solution is adding

type Add<A extends number, B extends number> = intrinsic
type Subtract<A extends number, B extends number> = intrinsic
type Increment<A extends number> = Add<A, 1>
type Decrement<A extends number> = Subtract<A, 1>

🍺

@RyanCavanaugh RyanCavanaugh changed the title Enhance members of literal type like const array type Enhance members of literal type like const array type (e.g. string literal types should have literal length property) Mar 8, 2021
@shorwood
Copy link

shorwood commented Jun 27, 2022

As pointed out, i think it would be nice to have some better typing possibilities on strings.

Use cases

// --- Improved getter
type FirstChar = 'foobar'[0] // 'f'
type LastChar = 'foobar'[-1] // 'r'

// --- Intrinsic type to get character at index
type FirstChar = CharAt<'foobar', 0>> // 'f'
type LastChar = CharAt<'foobar', -1> // 'r'

// --- Character ranges
type ARange = 'a0' ... 'a1' // 'a0' | 'a1' | 'a2' | 'a3' | 'a4' | 'a5' | 'a6' | 'a7' | 'a8' | 'a9'
type HexCharacter = 'a' ... 'f' | '0' ... '9' // 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

// --- Intrinsic types to restrict string character and size
type HexString = StringOf<HexCharacter>
type RGB = StringOf<HexCharacter, 6, 8>
type MD5 = StringOf<HexCharacter, 32>
type LenghtOf3 = StringOf<3>
type LenghtOf3To6 = StringOf<3, 6>

// --- Use cases
const hex: HexString = 'abcdef0123465789'
const rgb: RGB = 'ff0080'
const rgba: RGB = 'ff0080ff'
const MD5: RGB = '3858f62230ac3c915f300c664312c63f'

@sno2
Copy link
Contributor

sno2 commented Aug 16, 2022

Hello, for those looking for a more efficient solution, I've created a logarithmic implementation for anyone who is still facing this issue but can't use the linear implementation due to the poor performance of it https://gist.github.com/sno2/7dac868ec6d11abb75250ce5e2b36041

Edit: hmm I will consider a logarithmic string indexer as well

@dimitropoulos
Copy link
Contributor

would a PR on this be welcomed? If so, I'd like to give it a shot.

@castarco
Copy link

@augustobmoura

This

const lastChar = (str: Exclude<string, EmptyString>): SingleChar => str[str.length - 1]!;

wouldn't work well even if we narrowed down string literals' length, as Exclude only works well on union types. We'd need two new different features combined for this to work.

@castarco
Copy link

castarco commented Aug 13, 2024

Some extra comments from myself in this issue that I just closed for being (almost) duplicated:
#59615

About some proposals I've seen in this thread, some general comments:

  • I don't think that introducing new intrinsic types is a good idea. It goes against the TypeScript guidelines, and might be enough cause to refuse working on this idea. We should try to be as lean as possible when proposing new features if we want them to be accepted.

  • I'm also seeing some proposals that depend not only on one new feature (being able to statically assert the length of a string literal (or more generally, opaque strings)), but on many (for example, changing how Exclude works).

  • (In this thread) I'm missing a more clearcut view on why this would be useful for the majority of TypeScript developers, most examples that have been exposed in this thread are focused on niche cases that most developers don't care about. I admit that I'm weak against nerd snipping, and that I profoundly care about these edge cases, but it's not the minority who we should convince.

@augustobmoura
Copy link

wouldn't work well even if we narrowed down string literals' length, as Exclude only works well on union types. We'd need two new different features combined for this to work.

I agree, I wrote that comment a long time ago, and I think I was just throwing out ideas. I didn't go as far as defining formal specifications for it.

If we go the maximally minimal route, I would say that having literal strings carry their length information would already allow a bunch of interesting use-cases. What I'm saying is:

type Literal = 'string-literal'
type Len = Literal['length'] // this would be 14

Another way of implementing the "no empty strings" example could be:

const lastChar = <T extends string>(str: T extends { length: 0 } ? never : T): SingleChar => str[str.length - 1]!;

Other interesting examples with generics would be type guard functions for string length:

const ofSize = <T extends string, N extends number>(txt: T, size: N): txt is string & { length: N } =>
  txt.length === size
  
// then we could use it for narrowing types:
if (ofSize(txt, 1)) {
  console.log(lastChar(txt))
}

To be completely honest it still feels a bit janky, and I still have some open questions

  • How should we store the length value? Maybe have it be computed dynamically every time? We already have the whole string contents on literal types, so it should be possible
  • Should the type for literals be exactly the same as string & { length: N }? Otherwise, how can we declare a type which we only care about its length

At the end of the day, I ask myself how useful would this be? I sent my first comment on this thread more than 4 years ago, and since then didn't really have any compelling use-cases for it, besides the check for empty strings here and there. Maybe other use-cases would be for string formats, like ISO dates or UUIDs, but yet I'm not convinced it is worth the complications in implementation

@HansBrende
Copy link

I concur that length on its own would support many helpful use-cases. Besides simple stuff like type Char = string & {length: 1}, it would also make recursive template expressions which need to iterate a fixed number of times much easier to implement, since otherwise we have no way of knowing "when to stop" without doing complicated array destructuring to simulate counting. For example:

type IsRGB<S extends string> = S extends `#${infer R}` & {length: 4 | 7} ? IsHexString<R> : never

type IsWellFormedCurrencyCode<S extends string> = S extends {length: 3} ? IsAlphaString<S> : never

So even if none of the other proposals were implemented, string literal length would still be valuable to add.

That being said, there are ways to further enhance this behavior without creating new utility types.

For example:

// Synonymous with the "StringOf<HexCharacter>" idea mentioned above, 
// but implemented via an index signature instead of a utility type:
type HexString = string & {[_: number]: HexDigit}

Alternatively, we could implement the same thing via the existential types proposal:

type HexString = <exists S extends string> IsHexString<S> extends never ? never : S

Either way, that would allow us to do some pretty cool stuff with the string literal length:

type RGB = `#${HexString}` & {length: 4 | 7}

type XXXX = HexString & {length: 4}
type XXXXXXXX = HexString & {length: 8}
type XXXXXXXXXXXX = HexString & {length: 12}

type UUID = `${XXXXXXXX}-${XXXX}-${XXXX}-${XXXX}-${XXXXXXXXXXXX}` 

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