Repository navigation
Enhance members of literal type like const array type (e.g. string literal types should have literal length property) #34692
Description
Activity
MartinJohns commented
on Oct 24, 2019 ContributorMore actionsSounds like a duplicate of #34589.
Reacted by Jeongho NamNo, I don't think so - #34589 is about tuple types. This is about using literal types for the
.lengthand elements of a string literal.Reacted by Jeongho Nam, Martin Johns and Jimmy Zhening Luo- addedAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScriptAn idea for TypeScript
on Oct 30, 2019 RyanCavanaugh commented
on Oct 30, 2019 MemberMore actionsWould be nice to understand why this would be useful
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.
There is a lot of use cases
One use case is asserting that a method is only called for a single character, mimicking the
chartype 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
Reacted by Craig P Hicks, Sebastian Hädrich and JoepAnother 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 intype-plusand 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>
🍺
Reacted by Jeff Zou and Craig P Hicks- 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 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'
Reacted by Craig P Hicks, Hans Brende, Sebastian Hädrich, Joep and Eric Bieszczad-StieHello, 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
Reacted by Stanley Horwood, Homa Wong, Ponzu and Andrés Correa CasablancaReacted by Andrés Correa Casablancadimitropoulos commented
on Feb 29, 2024 ContributorMore actionswould a PR on this be welcomed? If so, I'd like to give it a shot.
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
Excludeonly works well on union types. We'd need two new different features combined for this to work.Some extra comments from myself in this issue that I just closed for being (almost) duplicated:
#59615About 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
Excludeworks). -
(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.
-
wouldn't work well even if we narrowed down string literals' length, as
Excludeonly 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
Reacted by Dimitri Mitropoulos and Sebastian HädrichI concur that
lengthon its own would support many helpful use-cases. Besides simple stuff liketype 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}`
Reacted by Sebastian Hädrich, Joep and Allan OricilIn 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']
But do note that it really only calculates the number of chunks TS will split a string into using
inferin template literals, and this is not necessarily going to be the same as the string'slengthproperty at runtime. See #63533. If TS had a literal numericlengthproperty 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.Reacted by snarbles2
The const array type, its principle members are also be the constant. When define a const array
[number, string, boolean], itslengthtype be3. Also, when access to special index, the type also points the exact const type.However, the literal type is not like the const array. When define a literal type "something", its
lengthbe not9butnumber. Also, when access to a special index, the type is not a special literal type but astringtype.What about enhancing the literal type to be like the const array type?