Skip to content

Normalize Specific String Length Behavior with Tuple's #43134

Closed
@sno2

Description

@sno2

Suggestion

πŸ” Search Terms

  • String#length
  • String
  • Intrinsic string length property
  • String.length type

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    This wouldn't change the runtime behavior of existing JavaScript code
    This could be implemented without emitting different JS based on the types of the expressions
    This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
    This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Currently, tuple types have the length property intrinsicly computed as shown in the folllowing:

type Names = ["Joe", "Jeff", "Jerry"];

type NamesLength = Names["length"]; // 3

However, for specific string types such as "Joe", the length property is still of type number:

type Name = "Joe";

type NameLength = Name["length"]; // number

I would like to propose that the length property be computed in the type type system such that Person["length"] (as shown in the previous example) would evaluate to 3 (the proper length of the string) instead of number.

type Name = "Joe";

type NameLength = Name["length"]; // 3

Now, time for a few of the hairy details.

One of them is how the behavior for the property would be when the type is a union of strings. The best way to define this behavior is by following what happens when you get the length property on a union of as defined in the following code:

type Params1 = [name: string];

type Params2 = [name: string, age: number];

type EitherParams = Params1 | Params2;

type EitherParamsLength = EitherParams["length"]; // 1 | 2

And the string behavior for getting the length of union strings should not contain any "magic" either, just give out a union of each of the string's length:

type Name = "Joe" | "Jeff" | "Robert";

type NameLength = Name["length"]; // 3 | 4 | 6
type Version = "1" | "1.1-alpha1" | "1.2.1" | "1.2.2";

type FormattedVersion = `v${Version}`; // "v1" | "v1.1-alpha1" | "v1.2.1" | "v1.2.2"

type FormattedVersionLength = FormattedVersionLength["length"]; // 2 | 11 | 6

The generic string type should also behave exactly like a generic Array type when you are getting the length.

interface Person {
  name: string;
  age: number;
}

type People = Person[];

type PeopleLength = People["length"]; // number
type Name = string;

type NameLength = Name["length"]; // number

With union types for the strings when combining string and a specific string type such as "hello", the string length should always remain supreme. This behavior is not exactly unexpected because unions like string | "foo" | "asdf" evalute to just string.

type Name = string | "foo" | "asdf"; // string

type NameLength = Name["length"]; // number

πŸ“ƒ Motivating Example

The feature would improve TypeScript because it makes the behavior of similarly-structured concepts (specific string and tuple) more universal. Currently, if I wanted to get the length of a string type, I would have to do something like the following:

/** Turns a string type into a tuple of the characters. */
type Chars<
  T extends string,
  $Chars extends string[] = []
> = T extends `${infer $Ch}${infer $Rest}`
  ? "" extends $Rest
    ? [...$Chars, $Ch]
    : Chars<$Rest, [...$Chars, $Ch]>
  : never;

type Name = "Carter";

type NameChars = Chars<Name>; // ["C", "a", "r", "t", "e", "r"]

type NameLength = Chars<Name>["length"]; // 6

However, the Chars type will easily overload due to its recursive nature:

/** Turns a string type into a tuple of the characters. */
type Chars<
  T extends string,
  $Chars extends string[] = []
> = T extends `${infer $Ch}${infer $Rest}`
  ? "" extends $Rest
    ? [...$Chars, $Ch]
    : Chars<$Rest, [...$Chars, $Ch]>
  : never;

type Name = "a not-even-that-long string";

type NameChars = Chars<Name>; // overload error

type NameLength = Chars<Name>["length"]; // unreachable

πŸ’» Use Cases

The ability to get the length of a string type just by accessing the string property has many benefits. One of which is being able to lock a string to a specific length for something such as a date in which you want to specify the number of characters that come in each part.

type ExtractDateParts<
  T extends string
> = T extends `${infer M}-${infer D}-${infer Y}`
  ? M["length"] | D["length"] extends 2
    ? Y["length"] extends 4
      ? { month: M; day: D; year: Y }
      : never
    : never
  : never;

type Valid1 = ExtractDateParts<"10-13-2001">; // { month: "10", day: "17", year: "2004" }

type Invalid1 = ExtractDateParts<"10-1-2001">; // never - day has a length of 1

type Invalid2 = ExtractDateParts<"101-12-2001">; // never - month has a length of 3

Activity

sno2

sno2 commented on Mar 8, 2021

@sno2
ContributorAuthor

I thought that the normalization of the indices of specific string types and tuples should not be included in this as it seemed too broad. However, if anyone would like to write out an issue for that that would normalize the type behaviors and allow for the following code to work as indicated then please do so:

type Name = "Carter";

type FirstLetter = Name[0]; // "C"

type AllLetters = Name[number]; // "C" | "a" | "r" | "t" | "e" | "r"

type OutOfBoundsLetter = Name[6]; // error: String type 'Name' has no character at index '6'. [Very similar to tuple out of bounds error]
MartinJohns

MartinJohns commented on Mar 8, 2021

@MartinJohns
Contributor

The ability to get the length of a string type just by accessing the string property has many benefits. One of which is being able to lock a string to a specific length for something such as a date in which you want to specify the number of characters that come in each part.

That sounds like a poor use-case for this, it won't prevent invalid strings to be passed. #41160 would work for this.

sno2

sno2 commented on Mar 8, 2021

@sno2
ContributorAuthor

The ability to get the length of a string type just by accessing the string property has many benefits. One of which is being able to lock a string to a specific length for something such as a date in which you want to specify the number of characters that come in each part.

That sounds like a poor use-case for this, it won't prevent invalid strings to be passed. #41160 would work for this.

Sorry, I was trying to explain one of the use-cases of being able to cap strings at a specific length. A use case that can be solved by having the length key intrinsically updated is setting the only length that cryptographic hex strings can hold. The solution would be the following:

type CryptoPublicKey = string & { length: 64 };

type CryptoSecretKey = string & { length: 128 };

const public: CryptoPublicKey = "b2adab9840a26690b15c24bd7f1b19d317468d06e21e84f6333350263bc62c5c";

const secret: CryptoSecretKey = "ea01f13a513086dffeeb13d871e8c4fc38a6b19e8d203969c650e992a84139a374505c0e5ac292e0bafab13021c8fa76c63bf4be1759b324b95a9c9c0f2be0da";

const badSecret: CryptoSecretKey = "ea0"; // error: type 'ea0' is not assignable to type 'string & { length: 128 }'.  '3' and '128' are not compatible.

You would basically have the power to not only get the length of a string, but also constrict its length without even requiring generics. Now, of course you wouldn't be able to verify that the contents of the string are valid in this case, but we are able to set the length of the string which is way better than nothing and can be a base for building better-typed strings. This code could then be modified slightly to solve #41160 (comment).

MartinJohns

MartinJohns commented on Mar 8, 2021

@MartinJohns
Contributor

Would a string containing only emojis be a valid CryptoSecretKey? I doubt so.

This just seems like the wrong approach. None of your suggestions would be solved by having a specific length on the string.

RyanCavanaugh

RyanCavanaugh commented on Mar 8, 2021

@RyanCavanaugh
Member

I absolutely know we have an issue on this already, but cannot find it 😒

MartinJohns

MartinJohns commented on Mar 8, 2021

@MartinJohns
Contributor

I absolutely know we have an issue on this already, but cannot find it 😒

I thought so as well, but couldn't find it either... until now: #34692 Turns out we both commented on it.

sno2

sno2 commented on Mar 8, 2021

@sno2
ContributorAuthor

Closing in favor of #34692

RyanCavanaugh

RyanCavanaugh commented on Mar 8, 2021

@RyanCavanaugh
Member

Thank you for saving my sanity @MartinJohns πŸ˜…

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

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @MartinJohns@RyanCavanaugh@sno2

        Issue actions

          Normalize Specific String Length Behavior with Tuple's Β· Issue #43134 Β· microsoft/TypeScript