typescript - Verktyg för att konvertera versaler till bindestreck fungerar utom när strängen börjar med
Jag har ett typverktyg DashUppercase som konverterar alla versaler i en sträng till ett - följt av motsvarande gemen. Det är typat enligt följande:
type LowerAlpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
type UpperAlpha = Uppercase<LowerAlpha>;
type Replace<S extends string, W extends string, P extends string> =
S extends '' ? '' : W extends '' ? S :
S extends `${infer F}${W}${infer E}` ? `${F}${P}${E}` : S
type DashUppercase<S extends string> = S extends `${infer START}${UpperAlpha}${infer REST}`
? DashUppercase<`${START}-${Uncapitalize<Replace<S, START, "">>}`>
: S;
Verktyget Replace låter mig enkelt extrahera START-strängen så att jag kan avkapitalisera resten.
Den gör exakt vad den är avsedd att göra när det första tecknet inte är en versal, men när strängliteralet som skickas in börjar med en versal så bara snurrar det:
Jag försökte faktiskt ta itu med detta genom att skapa ett villkor på längden av START:
export type StringLength<S extends string, A extends any[] = []> = S extends ''
? A['length']
: S extends `${infer First}${infer Rest}` ? StringLength<Rest, [First, ...A]> : never
export type DashUppercase<S extends string> = S extends `${infer START}${UpperAlpha}${infer REST}`
? StringLength<START> extends 0
? DashUppercase<`${Uncapitalize<S>}`>
: DashUppercase<`${START}-${Uncapitalize<Replace<S, START, "">>}`>
: S;
Tänkte att detta skulle ge mig en enkel möjlighet att konvertera det versala S till en gemen version innan jag började. Tyvärr är resultaten desamma. Kan någon hjälpa mig att hitta vad jag gör fel?
Lekplats
Svar #1
Det finns i stort sett två sätt att använda template literal-typer för att analysera strängar, och båda har förbehåll:
TECKEN-FÖR-TECKEN
Avsevärt det enklaste tillvägagångssättet är att gå igenom en sträng tecken för tecken, på detta sätt:
type Something<T extends string> =
T extends `${infer C0}${infer R}` ? Combine<C0, Something<R>> :
BaseCase
där C0 är den första tecknet i strängen och R är resten. När du har två infer-platshållare bredvid varandra kommer kompilatorn att härleda ett enda tecken för den första. Detta är enkelt eftersom du alltid vet vad som kommer att bli resultatet för C0. Och den största varningen är att rekursionsgränserna är tillräckligt grunda för att något liknande detta normalt endast fungerar för strängar upp till omkring 20 tecken långa. Du kan ändra det lite för att ungefär fördubbla eller tredubbla den maximala stränglängden som accepteras, genom att ta två eller tre tecken samtidigt där det är möjligt. Till exempel:
type Something<T extends string> =
T extends `${infer C0}${infer C1}${infer R}` ? Combine<C0, Combine<C1, Something<R>>> :
T extends `${infer C0}${infer R}` ? Combine<C0, Something<R>> :
BaseCase
Om vi närmar oss DashUppercase med denna metod, får vi:
type _DU<T extends string> = T extends Lowercase<T> ? T : `-${Lowercase<T>}`;
type DashUppercase<T extends string> =
T extends `${infer C0}${infer R}` ? `${_DU<C0>}${DashUppercase<R>}` :
""
vilket är ganska enkelt. Observera att istället för att hålla en lista över versala tecken, identifierar jag ett versalt tecken T som något som ändras när du tillämpar Lowercase<T> på det. Det fungerar för dina användningsfall (tror jag):
type A = DashUppercase<'oneTwoThree'> // "one-two-three"
type B = DashUppercase<'OneTwoThree'> // "-one-two-three"
type C = DashUppercase<'23Skidoo'> // "23-skidoo"
men, som jag sa, orsakar ganska långa strängar rekursionsvarningar:
type Oops =
DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // error, too long, excessively deep
Om jag ändrar det till två på två, får du:
type DashUppercase<T extends string> =
T extends `${infer C0}${infer C1}${infer R}` ?
`${_DU<C0>}${_DU<C1>}${DashUppercase<R>}` :
T extends `${infer C0}${infer R}` ? `${_DU<C0>}${DashUppercase<R>}` :
""
type Okay = DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // okay now
type Oops =
DashUppercase<'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw'> // too long
vilket kanske eller kanske inte är tillräckligt långt för ditt användningsområde.
AVGRÄNSARE-SPLITTRING
Det andra tillvägagångssättet, som är det du försöker göra, är att dela strängen vid avgränsare. Detta har troligen en sektion som ser ut som att T förlänger '$ {infer F}$ {D}$ {infer R}' i sig, där F är den första delen av strängen, D är någon avgränsare eller union av avgränsare, och R är resten av strängen. Det lockande med detta är att, förutsatt att din inmatningssträng har få avgränsare, kommer du inte att stöta på rekursionsgränser. Den största nackdelen är att det är komplicerat och svårt att veta vad som kommer att komma ut för F och R.
Kompilatorn tenderar att härleda unioner om det finns flera kandidater; inget hindrar F från att innehålla D. Detta är vad som orsakar problem i din definition. Så du kommer att vilja ta bort från F allt som innehåller en D själv, och sedan behöver du härleda resten av strängen en andra gång om du behöver identifiera avgränsaren (om D är en union och du bryr dig om vilken medlem som matchade).
I den här GitHub-kommentaren skrev jag ett generellt Break<T, D>-verktyg som tar en sträng T och delar upp den i en tupel av formen [F, R] där F är det längsta prefixet som inte innehåller D, och R är resten av strängen. Det ser ut så här:
type Break<T extends string, D extends string> = (
string extends T ? [string, string] : (
T extends `${infer F}${D}${infer R}` ? (
F extends `${infer X}${D}${infer Y}` ? never : (
T extends `${F}${infer R}` ? [F, R] : never
)
) : [T, ""]
)
);
Usch! Men det verkar faktiskt fungera.
Beväpnade med det kan vi skriva DashUppercase så här:
type DashUppercase<T extends string> =
Break<T, UpperAlpha> extends [infer L, infer R] ?
L extends string ? R extends `${infer U}${infer RR}` ?
`${L}-${Lowercase<U>}${DashUppercase<RR>}` : L :
never : never;
Så vi Break T med UpperAlpha-avgränsaren (vilket vi faktiskt behöver; en annan poäng mot denna metod, enligt min åsikt). Den returnerade L innehåller inga versaler, så vi kan alltid returnera den i början av strängen. R börjar antingen med en versal, som vi kan ta bort och transformera, eller så är den tom.
Detta fungerar nu för alla test från tidigare, inklusive de långa:
type A = DashUppercase<'oneTwoThree'> // "one-two-three"
type B = DashUppercase<'OneTwoThree'> // "-one-two-three"
type C = DashUppercase<'23Skidoo'> // "23-skidoo"
type Okay = DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // okay now
type Okay2 =
DashUppercase<'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw'> // also okay
Självklart, om du råkar få många versaler, kommer detta då att nå rekursionsgränserna, och ännu snabbare än tidigare på grund av de flera villkorskontrollerna i Break:
type Oops = DashUppercase<'ABCDEFGHIJKL'> // too long
Du kan kanske undvika denna gräns genom att växla fram och tillbaka mellan att söka efter versaler och att söka efter icke-versaler, men vid det här laget skulle jag hellre springa skrikande än försöka skriva det. Särskilt eftersom listan över "icke-versaler" är ganska lång.
Så där har du det. Du kan antingen analysera tecken individuellt eller i små grupper och få enkla definitioner som endast fungerar för ganska korta strängar, eller söka efter avgränsare och få invecklade och komplicerade definitioner som fungerar för längre strängar.
Lekplatslänk till kod
Svar #2
Följande del:
S extends `${infer START}${UpperAlpha}${infer REST}`
Agerar inte som du förväntade dig.
Om du ändrar din funktion för testning:
export type DashUppercase<S extends string> =
S extends `${infer START}${UpperAlpha}${infer REST}`
? START
: S;
Du kommer att se mycket märkliga resultat:
type T1 = DashUppercase<"Two">; // ""
type T2 = DashUppercase<"TwoThree">; // ""
type T3 = DashUppercase<"oneTwoThree">; // "one"
type T4 = DashUppercase<"oneTwoThreeFour">; // "oneTwoThree" | "one"
type X = DashUppercase<"OneTwoThree">; // "" | "One"
När samma versala bokstav upprepas flera gånger fungerar det som förväntat. Men när olika bokstäver är involverade returneras resultaten som en lista, för varje versal bokstav separat (i fallet med type X för "O" får du "" och för "T" får du "One", alltså "" | "One").
Fullständig lekplatslänk finns här: https://tsplay.dev/wXkK1W
Jag är dock inte säker på hur jag ska komma runt det här problemet. :(
