用对象中的值替换部分字符串

Replace Parts of Literal String with Values From Object

我在理解 Typescript magic 的某个特定位时遇到了问题。我想要一个像这样使用的 StringReplaceAll 泛型:

type Replaced = StringReplaceAll<"greeting adjective World!", { greeting: "Hello", adjective: "Nice" }>

结果:

type Replaced = "Hello Nice World!"

所以基本上 string.replaceAll() 用于文字。我让它与只有一个键(随后字符串中只有一个占位符)的对象一起工作,但我无法理解如何从那里继续。

解决方案

这是可能的。我使用了 中的 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"

稍后我可能会尝试找到解决此问题的方法。

Playground

这是实现 StringReplaceAll<T, M> 的一种方法,其中 T 是我们要操作的字符串 literal typeM 是字符串 [的“映射”对象=73=] 对替换:

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

请注意,这是一个 tail recursive conditional typeA 作为最终结果的累加器;它以空字符串类型 "" 开始,我们在递归时将内容连接到它上面,最后在我们从 T 中得到 运行 时生成任何 A

这个思路是:

首先我们看看 T 是否以 Mmapper 的键开头。如果是这样,我们将 T 拆分为该键 K 和字符串的其余部分 R。在实践中,需要两个 conditional types with infer in them 才能做到这一点,首先是 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" } 那么该算法可能会产生所有可能替换的并集以及其他一些不是的东西完全可能,因为切入 KR 并不顺利:

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"

重要吗?我希望不会,因为解决这个问题会使事情变得更加复杂,因为我们试图找到(比如说)匹配的 longest 键。在任何情况下,我都不会太担心这种情况,但请记住,对于任何复杂的类型操作,您都应该确保针对您关心的用例进行全面测试。

Playground link to code