Skip to content

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

Description

@samchon

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?

Activity

  1. MartinJohns commented on Oct 24, 2019

    @MartinJohns
    Contributor

    Sounds like a duplicate of #34589.

  2. fatcerberus commented on Oct 24, 2019

    @fatcerberus

    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.

  3. RyanCavanaugh commented on Oct 30, 2019

    @RyanCavanaugh
    Member

    Would be nice to understand why this would be useful

  4. samchon commented on Oct 31, 2019

    @samchon
    Author

    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;

    Ryan Cavanaugh (@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.

  5. augustobmoura commented on Feb 6, 2020

    @augustobmoura

    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

  6. unional commented on Jan 22, 2021

    @unional
    Contributor

    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>

    🍺

  7. 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)[/+] on Mar 8, 2021
  8. shorwood commented on Jun 27, 2022

    @shorwood

    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'
  9. sno2 commented on Aug 16, 2022

    @sno2
    Contributor

    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

  10. dimitropoulos commented on Feb 29, 2024

    @dimitropoulos
    Contributor

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

  11. castarco commented on Aug 13, 2024

    @castarco

    Augusto Moura (@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.

  12. castarco commented on Aug 13, 2024

    @castarco

    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.

  13. augustobmoura commented on Aug 13, 2024

    @augustobmoura

    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

  14. HansBrende commented on Dec 14, 2024

    @HansBrende
    Contributor

    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}` 
  15. jcalz commented on Jun 7, 2026

    @jcalz
    Contributor

    In case it matters here is my version of a recursive utility type for determining string literal lengths, works up to strings of length 8191 before TS gets angry about stuff:

    type Strlen<T extends string,
        D0 extends string = string & {}, DR extends string[] = [],
        A0 extends any[] = [0], AR extends any[][] = []> =
        T extends `${D0}${string}` ?
        Strlen<T, `${D0}${D0}`, [D0, ...DR], [...A0, ...A0], [A0, ...AR]> :
        _S<T, DR, AR>
    
    type _S<T extends string, D extends string[], A extends any[][],
        O extends any[] = []> =
        [D, A] extends [[infer D0 extends string, ...infer DR extends string[]],
            [infer A0 extends any[], ...infer AR extends any[][]]] ?
        T extends `${D0}${infer R}` ?
        _S<R, DR, AR, [...O, ...A0]> : _S<T, DR, AR, O> :
        O['length']

    Playground link

    But do note that it really only calculates the number of chunks TS will split a string into using infer in template literals, and this is not necessarily going to be the same as the string's length property at runtime. See #63533. If TS had a literal numeric length property for string literals then that would presumably be the number of UTF-16 code units in the string, whereas anything computed with template literal strings will be whatever you get. Right now with tsc, it's number of UTF-16 code units. With tsgo it's likely to be number of Unicode code points.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions