#typescript #typescript-generics
Вопрос:
У меня есть утилита типа DashUppercase, которая преобразует любые символы верхнего регистра в строке в a -
, за которым следует эквивалент нижнего регистра. Он набран следующим образом:
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;
Утилита замены simple позволяет мне извлечь начальную строку, чтобы я мог снять заглавные буквы с оставшейся.
Он делает именно то, для чего предназначен, когда первый символ не является заглавной буквой, но когда переданный строковый литерал начинается с заглавной буквы, он просто вращается:
Я попытался решить эту проблему, создав условие о длине 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;
Решил, что это даст мне легкую возможность преобразовать заглавную S
версию в некапитализированную перед запуском. К сожалению, результаты те же самые. Кто-нибудь может помочь мне понять, что я делаю не так?
Ответ №1:
В целом существует два способа использования шаблонных литеральных типов для анализа строк, и оба они имеют предостережения:
ОТ ПЕРСОНАЖА К ПЕРСОНАЖУ
Безусловно, самый простой подход состоит в том, чтобы пройти строку по символу, как это:
type Something<T extends string> =
T extends `${infer C0}${infer R}` ? Combine<C0, Something<R>> :
BaseCase
где C0
первый символ строки и R
остальные. Когда у вас есть два infer
заполнителя рядом друг с другом, компилятор выведет один символ для первого. Это просто, потому что вы всегда знаете, что из этого выйдет C0
. И главная оговорка заключается в том, что пределы рекурсии достаточно неглубоки, чтобы что-то подобное работало только для строк длиной до 20 или около того. Вы можете немного изменить его, чтобы примерно удвоить или утроить максимальную принятую длину строки, захватив, где это возможно, два или три символа сразу. Например:
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
Если мы подойдем DashUppercase
с помощью этого метода, мы получим:
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>}` :
""
что довольно просто. Обратите внимание, что вместо того, чтобы вести список заглавных символов, я определяю заглавный символ T
как нечто, что меняется при обращении Lowercase<T>
к нему. Это работает для ваших случаев использования (я думаю):
type A = DashUppercase<'oneTwoThree'> // "one-two-three"
type B = DashUppercase<'OneTwoThree'> // "-one-two-three"
type C = DashUppercase<'23Skidoo'> // "23-skidoo"
но, как я уже сказал, длинные строки вызывают предупреждения о рекурсии:
type Oops =
DashUppercase<'abcdefghijklmnopqrstuvwxyz'> // error, too long, excessively deep
Если я изменю его на два на два, вы получите:
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
который может быть или не быть достаточно длинным для вашего варианта использования.
РАЗДЕЛИТЕЛЬ-РАЗДЕЛЕНИЕ
Другой подход, который вы пытаетесь сделать, состоит в том, чтобы разделить строку на разделители. Здесь, вероятно, есть раздел, который выглядит так, как T extends `${infer F}${D}${infer R}`
в нем, где F
первая часть строки- D
это какой-то разделитель или объединение разделителей, а R
остальная часть строки. Привлекательность этого заключается в том, что, если предположить, что в вашей входной строке мало разделителей, вы не столкнетесь с ограничениями рекурсии. Основным недостатком является то, что сложно и трудно понять, что выйдет за F
и R
.
Компилятор будет склонен выводить союзы здесь, если есть несколько кандидатов; ничто F
не мешает содержать D
. Это то, что кусает вас в вашем определении. Таким образом, вы захотите исключить F
все, что содержит D
само по себе, а затем вам нужно будет вывести остальную часть строки во второй раз, если вам нужно определить разделитель (если D
это объединение, и вам важно, какой член соответствует).
В этом комментарии на GitHub я написал общую Break<T, D>
утилиту , которая берет строку T
и разбивает ее на кортеж формы [F, R]
, где F
самый длинный префикс не содержит D
, а 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, ""]
)
);
Фу! Но, похоже, это работает.
Вооружившись этим, мы можем написать DashUppercase
вот так:
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;
Поэтому мы Break
T
используем UpperAlpha
набор разделителей (который нам действительно нужен; еще один пункт против этого метода, на мой взгляд). Возвращаемое L
значение не содержит символов верхнего регистра, поэтому мы всегда можем вернуть его в начале строки. R
либо начинается с прописного символа, который мы можем снять и преобразовать, либо он пуст.
Теперь это работает для всех предыдущих тестов, включая длинные:
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
Конечно, если в конечном итоге у вас будет много символов верхнего регистра, это приведет к ограничениям рекурсии, и даже раньше, чем раньше, из-за множественных условных проверок Break
:
type Oops = DashUppercase<'ABCDEFGHIJKL'> // too long
Возможно, вы сможете избежать этого ограничения, переключаясь между поиском символов в верхнем регистре и поиском символов без верхнего регистра, но на данный момент я бы предпочел кричать, чем пытаться это написать. Тем более, что список «не прописных символов» довольно длинный.
Так что поехали. Вы можете либо анализировать символы по отдельности или небольшими группами и получать простые определения, которые работают только для довольно коротких строк, либо искать разделители и получать запутанные и сложные определения, которые работают для более длинных строк.
Комментарии:
1. им нужно начать раздавать Нобелевские премии за вклад в рукописи, и я предполагаю, что вы станете первым победителем!
2. единственное, что было бы здорово-но я думаю, что это, вероятно, невозможно, — это переключиться на более широкий тип «строка» в тот момент, когда рекурсия достигнет своего предела.
Ответ №2:
Следующая часть:
S extends `${infer START}${UpperAlpha}${infer REST}`
Ведет себя не так, как вы ожидали.
Если вы измените свою функцию для тестирования:
export type DashUppercase<S extends string> =
S extends `${infer START}${UpperAlpha}${infer REST}`
? START
: S;
Вы увидите очень странные результаты:
type T1 = DashUppercase<"Two">; // ""
type T2 = DashUppercase<"TwoThree">; // ""
type T3 = DashUppercase<"oneTwoThree">; // "one"
type T4 = DashUppercase<"oneTwoThreeFour">; // "oneTwoThree" | "one"
type X = DashUppercase<"OneTwoThree">; // "" | "One"
Когда одна и та же заглавная буква повторяется несколько раз, она работает так, как ожидалось. Но когда задействованы разные буквы, результаты возвращаются в виде массива для каждой заглавной буквы отдельно (в случае type X
"O"
, если вы получаете ""
и для "T"
вас получаете "One"
, так "" | "One"
что .
Полная ссылка на игровую площадку находится здесь: https://tsplay.dev/wXkK1W
Однако я не уверен, как обойти эту проблему. 🙁
Комментарии:
1. странно, однако, что это работает, когда первый символ не в верхнем регистре
2. Это работает только случайно, потому что в вашем тестовом примере
"oneTwoThree"
две заглавные буквы совпадаютT
. Если бы вы попытались"twoThreeFour"
, код бы сломался.3. ах да, теперь я вижу закономерность