TypeScript - החלף חלקים ממחרוזת מילולית עם ערכים מאובייקט
אני נתקל בקושי להבין חלק מסוים של קסם ב-Typesscript. אני רוצה ליצור גנרי StringReplaceAll שמשמש כך:
type Replaced = StringReplaceAll<"greeting adjective World!", { greeting: "Hello", adjective: "Nice" }>
ומביא לתוצאה:
type Replaced = "Hello Nice World!"
אז בעצם מדובר ב-string.replaceAll() עבור ליטרלים. יש לי את זה עובד עם אובייקטים שיש להם רק מפתח אחד (ולכן רק מחליף אחד במחרוזת), אבל אני לא מצליח להבין איך להמשיך מכאן.
תשובה #1
הנה גישה אחת ליישום StringReplaceAll<T, M> כאשר T הוא סוג המחרוזת הליטרלית שאנו רוצים לערוך, וכאשר M הוא אובייקט "מיפוי" של זוגות מפתח-ערך של מחרוזות להחלפה:
type StringReplaceAll<T extends string, M extends { [k: string]: string },
A extends string = ""> =
T extends `${Extract<keyof M, string>}${infer R}` ? (
T extends `${infer K}${R}` ?
StringReplaceAll<R, M, `${A}${M[Extract<K, keyof M>]}`>
: never
) : T extends `${infer F}${infer R}` ? StringReplaceAll<R, M, `${A}${F}`> : A
שימו לב שזו סוג מותנה רקורסיבי זנב עם A כממלא עבור התוצאה הסופית; הוא מתחיל כסוג המחרוזת הריקה "" ואנו מחברים עליו דברים בזמן שאנחנו חוזרים בו, ולבסוף מייצרים את מה שהוא A כאשר נגמר לנו T.
הרעיון של זה הוא:
ראשית אנחנו בודקים אם T מתחיל במפתח של המיפוי M. אם כן, אנחנו מחלקים את T לאותו מפתח K ולשאר המחרוזת R. בפועל, זה דורש שני טיפוסים תנאיים עם infer בתוכם כדי שזה יקרה, כדי לקבל קודם את R ואחר כך את K. בכל מקרה, ברגע שאנחנו עושים זאת, אנחנו מחברים את M[K] לסוף A וממשיכים ברקורסיה. זו החלק של ההחלפה.
מצד שני, אם T לא מתחיל במפתח של M, אז פשוט נחלק אותו לתו הראשון F ושאר המחרוזת R, אם אפשרי. ואנחנו מצריפים את F לסוף A ומבצעים רקורסיה נוספת. זה פשוט מעתיק את התו הנוכחי בלי להחליף אותו.
לבסוף, אם T לא מתחיל במפתח של M ולא ניתן אפילו לחלק אותו לפחות לתו אחד, אז הוא ריק, ואתה סיים... ואנחנו פשוט מייצרים A.
בואו נדאג שזה יעבוד:
type Replaced =
StringReplaceAll<"greeting adjective World!", { greeting: "Hello", adjective: "Nice" }>
// type Replaced = "Hello Nice World!"
נראה טוב!
שים לב שאני בטוח שיש מקרים קיצוניים מוזרים; אם יש לך מפתחות תחליף שאחד מהם הוא קידומת של אחר (למשל, key1.startsWith(key2) הוא נכון), כמו { cap: "hat", cape: "shirt" } אז האלגוריתם כנראה יפיק איחוד של כל החלופות האפשריות יחד עם כמה דברים אחרים שאינם אפשריים לחלוטין משום שהחיתוך ל-K ול-R לא התבצע בצורה חלקה:
type Hmm = StringReplaceAll<"I don my cap and cape", { cap: "hat", cape: "shirt" }>;
// type Hmm = "I don my hat and hat" | "I don my hat and shirt" |
// "I don my hat and hate" | "I don my hat and shirte"
זה משנה? אני מקווה שלא, כי לתקן את זה יהפוך את הדברים למורכבים עוד יותר, בזמן שאנחנו מנסים למצוא (נגיד) את המפתח הארוך ביותר שמתאים. בכל מקרה, אני לא הולך לדאוג יותר מדי לגבי מצבים כאלה, אבל תזכור שלכל טיפול מורכב בסוגים צריך לוודא לבדוק באופן מלא כנגד מקרי שימוש שמעניינים אותך.
קישור למזחלת לקוד
תשובה #2
פתרון
זה אפשרי. השתמשתי ב-TuplifyUnion מכאן. ראה את ההתקפה מהתשובה המקושרת.
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type LastOf<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never
type Push<T extends any[], V> = [...T, V];
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>
type _StringReplaceAll<S extends string, TU extends any[], T extends Record<string, string>> =
TU[0] extends infer TU0
? {
[KEY in keyof TU0]: KEY extends string
? S extends `${infer L}${KEY}${infer R}`
? TU extends [any, ...infer REST]
? REST extends [any]
? _StringReplaceAll<`${L}${T[KEY]}${R}`, REST, T>
: `${L}${T[KEY]}${R}`
: `${L}${T[KEY]}${R}`
: S
: never
}[keyof TU0]
: never
type StringReplaceAll<
S extends string,
T extends Record<string, string>
> = _StringReplaceAll<S, TuplifyUnion<{
[K in keyof T]: Record<K, T[K]>
}[keyof T]>, T>
שימוש :
type Replaced = StringReplaceAll<"greeting adjective World!", { greeting: "Hello", adjective: "Nice" }>
// type Replaced = "Hello Nice World!"
הסבר
האלגוריתם די פשוט: אנו מכריזים על שלושה סוגים כלליים. S הוא המחרוזת שבה מתבצע ההחלפה. T הוא האובייקט עם מחרוזות ההחלפה ו-TU הוא טופל שמורכב מ-T.
למה הטיפל? על ידי המרת T לטיפל, נוכל לעבור על אלמנטים של הטיפל בצורה רקורסיבית. אנו לוקחים את האלמנט הראשון של T, מבצעים את החלפת המחרוזת ואז קוראים לטיפוס בצורה רקורסיבית עם שאר האלמנטים.
זה מוביל למאפיין חשוב. החלפת מחרוזת מתבצעת לפי סדר מהתכונה הראשונה לתכונה האחרונה.
יתרונות וחסרונות
הסוג StringReplaceAll תמיד מבטיח להפיק מחרוזת אחת בלבד. בהסתמך על הדוגמה של @jcalz, זה ייראה כך:
type Hmm = StringReplaceAll<"I don my cap and cape", { cap: "hat", cape: "shirt" }>
// type Hmm = "I don my hat and shirt"
אבל יש גם חסרון: כל מחרוזת מתאימה מחליפה רק את ההתרחשות הראשונה:
type Hmm2 = StringReplaceAll<"I don my cap and cap", { cap: "hat" }>
// type Hmm2 = "I don my hat and cap"
אני עשוי לנסות למצוא דרך לתקן את זה גם מאוחר יותר.
מִגרָשׂ מְשָׂחָק
