TypeScript 4.1 新特性:字符串模板類型,Vuex 終於有救了?

TypeScript 4.1 快要發佈了,老爺子 Anders Hejlsberg[1] 加入了一項重大更新,「字符串模板類型」 的支持。昨天看到這個更新的我特別興奮,曾幾何時,只要一遇到字符串拼接相關的類型,TypeScript 就束手無策了,比如:

  • Vuex 中加了 namespace 以後,dispatch 一個 mutation type 會帶上前綴 dispatch('cart/add')

  • lodash 的 get 方法,可以對一個對象進行 get(obj, 'a.b.c') 這樣的讀取。

現在 4.1 加入的這個新功能讓這一切都擁有了可能。

基礎語法

它的語法和 es 裏的字符串模板很相似,所以上手成本也很低,先看幾個例子:

type EventName<T extends string> = `${T}Changed`;
type T0 = EventName<'foo'>;  // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged'
type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`;
type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld'

字符串模板中的聯合類型會被展開後排列組合:

type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

新增關鍵字

爲了這個功能,老爺子在 TS 中新增了 uppercaselowercasecapitalizeuncapitalize 這些關鍵字,用於對模板粒度字符串變量進行處理。

type Cases<T extends string> = `${uppercase T} ${lowercase T} ${capitalize T} ${uncapitalize T}`;
type T11 = Cases<'bar'>;  // 'BAR bar Bar bar'

其實很簡單,就是提供了幾個處理方法:大寫、小寫,首字母大寫,首字母小寫。

配合 infer

特別強大的一點是,模板字符串可以通過 infer 關鍵字,實現類似於正則匹配提取的功能:

type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown;
type T20 = MatchPair<'[1,2]'>;  // ['1', '2']
type T21 = MatchPair<'[foo,bar]'>;  // ['foo', 'bar']

通過 , 分割左右兩邊,再在左右兩邊分別用一個 infer 泛型接受推斷值 [${infer A},${infer B}],就可以輕鬆的重新組合 , 兩邊的字符串。

配合 ... 拓展運算符和 infer遞歸,甚至可以實現 Join 功能:

type Join<T extends (string | number | boolean | bigint)[], D extends string> =
    T extends [] ? '' :
    T extends [unknown] ? `${T[0]}` :
    T extends [unknown, ...infer U] ? `${T[0]}${D}${Join<U, D>}` :
    string;
type T30 = Join<[1, 2, 3, 4], '.'>;  // '1.2.3.4'
type T31 = Join<['foo', 'bar', 'baz'], '-'>;  // 'foo-bar-baz'

實戰運用

實現 Vuex namespace 推斷:

type VuexOptions<M, N> = {
   namespace?: N,
   mutations: M,
}

type Action<M, N> = N extends string ? `${N}/${keyof M & string}` : keyof M

type Store<M, N> = {
   dispatch(action: Action<M, N>): void
}

declare function Vuex<MN>(options: VuexOptions<M, N>): Store<MN>

const store = Vuex({
   namespace: "cart" as const,
   mutations: {
      add() { },
      remove() { }
   }
})

store.dispatch("cart/add")
store.dispatch("cart/remove")

前往 Playground[2] 嘗試一下~

實現 lodash get 函數:

type PropType<T, Path extends string> =
    string extends Path ? unknown :
    Path extends keyof T ? T[Path] :
    Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
    unknown;

declare function get<TP extends string>(obj: T, path: P): PropType<TP>;

const obj = { a: { b: {c: 42, d: 'hello' }}};

const value = get(obj, "a.b.c")

前往 Playground[3] 嘗試一下~

總結

TypeScript 4.1 帶來的這個新功能讓 TS 支持更多字符串相關的拼接場景,其實是特別實用的,希望各位看了以後都能有所收穫~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章