精讀《Typescript 4》

1 引言

隨着 Typescript 4 Beta 的發佈,又帶來了許多新功能,其中 Variadic Tuple Types 解決了大量重載模版代碼的頑疾,使得此次更新很是有意義。前端

2 簡介

可變元組類型

考慮 concat 場景,接收兩個數組或者元組類型,組成一個新數組:node

function concat(arr1, arr2) {
 return [...arr1, ...arr2]; } 複製代碼

若是要定義 concat 的類型,以往咱們會經過枚舉的方式,先枚舉第一個參數數組中的每一項:react

function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A]; function concat<A, B>(arr1: [A, B], arr2: []): [A, B]; function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C]; function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D]; function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E]; function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];) 複製代碼

再枚舉第二個參數中每一項,若是要完成全部枚舉,僅考慮數組長度爲 6 的狀況,就要定義 36 次重載,代碼幾乎不可維護:git

function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2]; function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2]; function concat<A1, B1, C1, A2>(  arr1: [A1, B1, C1],  arr2: [A2] ): [A1, B1, C1, A2]; function concat<A1, B1, C1, D1, A2>(  arr1: [A1, B1, C1, D1],  arr2: [A2] ): [A1, B1, C1, D1, A2]; function concat<A1, B1, C1, D1, E1, A2>(  arr1: [A1, B1, C1, D1, E1],  arr2: [A2] ): [A1, B1, C1, D1, E1, A2]; function concat<A1, B1, C1, D1, E1, F1, A2>(  arr1: [A1, B1, C1, D1, E1, F1],  arr2: [A2] ): [A1, B1, C1, D1, E1, F1, A2]; 複製代碼

若是咱們採用批量定義的方式,問題也不會獲得解決,由於參數類型的順序得不到保證:github

function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;
複製代碼

在 Typescript 4,能夠在定義中對數組進行解構,經過幾行代碼優雅的解決可能要重載幾百次的場景:web

type Arr = readonly any[];
 function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {  return [...arr1, ...arr2]; } 複製代碼

上面例子中,Arr 類型告訴 TS TU 是數組類型,再經過 [...T, ...U] 按照邏輯順序依次拼接類型。typescript

再好比 tail,返回除第一項外剩下元素:json

function tail(arg) {
 const [_, ...result] = arg;  return result; } 複製代碼

一樣告訴 TS T 是數組類型,且 arr: readonly [any, ...T] 申明瞭 T 類型表示除第一項其他項的類型,TS 可自動將 T 類型關聯到對象 restredux

function tail<T extends any[]>(arr: readonly [any, ...T]) {
 const [_ignored, ...rest] = arr;  return rest; }  const myTuple = [1, 2, 3, 4] as const; const myArray = ["hello", "world"];  // type [2, 3, 4] const r1 = tail(myTuple);  // type [2, 3, ...string[]] const r2 = tail([...myTuple, ...myArray] as const); 複製代碼

另外以前版本的 TS 只能將類型解構放在最後一個位置:數組

type Strings = [string, string];
type Numbers = [number, number];  // [string, string, number, number] type StrStrNumNum = [...Strings, ...Numbers]; 複製代碼

若是你嘗試將 [...Strings, ...Numbers] 這種寫法,將會獲得一個錯誤提示:

A rest element must be last in a tuple type.
複製代碼

但在 Typescript 4 版本支持了這種語法:

type Strings = [string, string];
type Numbers = number[];  // [string, string, ...Array<number | boolean>] type Unbounded = [...Strings, ...Numbers, boolean]; 複製代碼

對於再複雜一些的場景,例如高階函數 partialCall,支持必定程度的柯里化:

function partialCall(f, ...headArgs) {
 return (...tailArgs) => f(...headArgs, ...tailArgs); } 複製代碼

咱們能夠經過上面的特性對其進行類型定義,將函數 f 第一個參數類型定義爲有順序的 [...T, ...U]

type Arr = readonly unknown[];
 function partialCall<T extends Arr, U extends Arr, R>(  f: (...args: [...T, ...U]) => R,  ...headArgs: T ) {  return (...b: U) => f(...headArgs, ...b); } 複製代碼

測試效果以下:

const foo = (x: string, y: number, z: boolean) => {};
 // This doesn't work because we're feeding in the wrong type for 'x'. const f1 = partialCall(foo, 100); // ~~~ // error! Argument of type 'number' is not assignable to parameter of type 'string'.  // This doesn't work because we're passing in too many arguments. const f2 = partialCall(foo, "hello", 100, true, "oops"); // ~~~~~~ // error! Expected 4 arguments, but got 5.  // This works! It has the type '(y: number, z: boolean) => void' const f3 = partialCall(foo, "hello");  // What can we do with f3 now?  f3(123, true); // works!  f3(); // error! Expected 2 arguments, but got 0.  f3(123, "hello"); // ~~~~~~~ // error! Argument of type '"hello"' is not assignable to parameter of type 'boolean' 複製代碼

值得注意的是,const f3 = partialCall(foo, "hello"); 這段代碼因爲尚未執行到 foo,所以只匹配了第一個 x:string 類型,雖而後面 y: number, z: boolean 也是必選,但由於 foo 函數還未執行,此時只是參數收集階段,所以不會報錯,等到 f3(123, true) 執行時就會校驗必選參數了,所以 f3() 時纔會提示參數數量不正確。

元組標記

下面兩個函數定義在功能上是同樣的:

function foo(...args: [string, number]): void {
 // ... }  function foo(arg0: string, arg1: number): void {  // ... } 複製代碼

但仍是有微妙的區別,下面的函數對每一個參數都有名稱標記,但上面經過解構定義的類型則沒有,針對這種狀況,Typescript 4 支持了元組標記:

type Range = [start: number, end: number];
複製代碼

同時也支持與解構一塊兒使用:

type Foo = [first: number, second?: string, ...rest: any[]];
複製代碼

Class 從構造函數推斷成員變量類型

構造函數在類實例化時負責一些初始化工做,好比爲成員變量賦值,在 Typescript 4,在構造函數裏對成員變量的賦值能夠直接爲成員變量推導類型:

class Square {
 // Previously: implicit any!  // Now: inferred to `number`!  area;  sideLength;   constructor(sideLength: number) {  this.sideLength = sideLength;  this.area = sideLength ** 2;  } } 複製代碼

若是對成員變量賦值包含在條件語句中,還能識別出存在 undefined 的風險:

class Square {
 sideLength;   constructor(sideLength: number) {  if (Math.random()) {  this.sideLength = sideLength;  }  }   get area() {  return this.sideLength ** 2;  // ~~~~~~~~~~~~~~~  // error! Object is possibly 'undefined'.  } } 複製代碼

若是在其餘函數中初始化,則 TS 不能自動識別,須要用 !: 顯式申明類型:

class Square {
 // definite assignment assertion  // v  sideLength!: number;  // ^^^^^^^^  // type annotation   constructor(sideLength: number) {  this.initialize(sideLength);  }   initialize(sideLength: number) {  this.sideLength = sideLength;  }   get area() {  return this.sideLength ** 2;  } } 複製代碼

短路賦值語法

針對如下三種短路語法提供了快捷賦值語法:

a &&= b; // a = a && b
a ||= b; // a = a || b a ??= b; // a = a ?? b 複製代碼

catch error unknown 類型

Typescript 4.0 以後,咱們能夠將 catch error 定義爲 unknown 類型,以保證後面的代碼以健壯的類型判斷方式書寫:

try {
 // ... } catch (e) {  // error!  // Property 'toUpperCase' does not exist on type 'unknown'.  console.log(e.toUpperCase());   if (typeof e === "string") {  // works!  // We've narrowed 'e' down to the type 'string'.  console.log(e.toUpperCase());  } } 複製代碼

PS:在以前的版本,catch (e: unknown) 會報錯,提示沒法爲 error 定義 unknown 類型。

自定義 JSX 工廠

TS 4 支持了 jsxFragmentFactory 參數定義 Fragment 工廠函數:

{
 "compilerOptions": {  "target": "esnext",  "module": "commonjs",  "jsx": "react",  "jsxFactory": "h",  "jsxFragmentFactory": "Fragment"  } } 複製代碼

還能夠經過註釋方式覆蓋單文件的配置:

// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect. /** @jsx h */ /** @jsxFrag Fragment */  import { h, Fragment } from "preact";  let stuff = (  <>  <div>Hello</div>  </> ); 複製代碼

以上代碼編譯後解析結果以下:

// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect. /** @jsx h */ /** @jsxFrag Fragment */ import { h, Fragment } from "preact"; let stuff = h(Fragment, null, h("div", null, "Hello")); 複製代碼

其餘升級

其餘的升級快速介紹:

構建速度提高,提高了 --incremental + --noEmitOnError 場景的構建速度。

支持 --incremental + --noEmit 參數同時生效。

支持 @deprecated 註釋, 使用此註釋時,代碼中會使用 刪除線 警告調用者。

局部 TS Server 快速啓動功能, 打開大型項目時,TS Server 要準備好久,Typescript 4 在 VSCode 編譯器下作了優化,能夠提早對當前打開的單文件進行部分語法響應。

優化自動導入, 如今 package.json dependencies 字段定義的依賴將優先做爲自動導入的依據,而再也不是遍歷 node_modules 導入一些非預期的包。

除此以外,還有幾個 Break Change:

lib.d.ts 類型升級,主要是移除了 document.origin 定義。

覆蓋父 Class 屬性的 getter 或 setter 如今都會提示錯誤。

經過 delete 刪除的屬性必須是可選的,若是試圖用 delete 刪除一個必選的 key,則會提示錯誤。

3 精讀

Typescript 4 最大亮點就是可變元組類型了,但可變元組類型也不能解決全部問題。

拿筆者的場景來講,函數 useDesigner 做爲自定義 React Hook 與 useSelector 結合支持 connect redux 數據流的值,其調用方式是這樣的:

const nameSelector = (state: any) => ({
 name: state.name as string, });  const ageSelector = (state: any) => ({  age: state.age as number, });  const App = () => {  const { name, age } = useDesigner(nameSelector, ageSelector); }; 複製代碼

nameage 是 Selector 註冊的,內部實現方式必然是 useSelector + reduce,但類型定義就麻煩了,經過重載能夠這麼作:

import * as React from 'react';
import { useSelector } from 'react-redux';  type Function = (...args: any) => any;  export function useDesigner(); export function useDesigner<T1 extends Function>(  t1: T1 ): ReturnType<T1> ; export function useDesigner<T1 extends Function, T2 extends Function>(  t1: T1,  t2: T2 ): ReturnType<T1> & ReturnType<T2> ; export function useDesigner<  T1 extends Function,  T2 extends Function,  T3 extends Function >(  t1: T1,  t2: T2,  t3: T3,  t4: T4, ): ReturnType<T1> &  ReturnType<T2> &  ReturnType<T3> &  ReturnType<T4> & ; export function useDesigner<  T1 extends Function,  T2 extends Function,  T3 extends Function,  T4 extends Function >(  t1: T1,  t2: T2,  t3: T3,  t4: T4 ): ReturnType<T1> &  ReturnType<T2> &  ReturnType<T3> &  ReturnType<T4> & ; export function useDesigner(...selectors: any[]) {  return useSelector((state) =>  selectors.reduce((selected, selector) => {  return {  ...selected,  ...selector(state),  };  }, {})  ) as any; } 複製代碼

能夠看到,筆者須要將 useDesigner 傳入的參數經過函數重載方式一一傳入,上面的例子只支持到了三個參數,若是傳入了第四個參數則函數定義會失效,所以業界作法通常是定義十幾個重載,這樣會致使函數定義很是冗長。

但參考 TS4 的例子,咱們能夠避免類型重載,而經過枚舉的方式支持:

type Func = (state?: any) => any;
type Arr = readonly Func[];  const useDesigner = <T extends Arr>(  ...selectors: T ): ReturnType<T[0]> &  ReturnType<T[1]> &  ReturnType<T[2]> &  ReturnType<T[3]> => {  return useSelector((state) =>  selectors.reduce((selected, selector) => {  return {  ...selected,  ...selector(state),  };  }, {})  ) as any; }; 複製代碼

能夠看到,最大的變化是不須要寫四遍重載了,但因爲場景和 concat 不一樣,這個例子返回值不是簡單的 [...T, ...U],而是 reduce 的結果,因此目前還只能經過枚舉的方式支持。

固然可能存在不用枚舉就能夠支持無限長度的入參類型解析的方案,因筆者水平有限,暫未想到更好的解法,若是你有更好的解法,歡迎告知筆者。

4 總結

Typescript 4 帶來了更強類型語法,更智能的類型推導,更快的構建速度以及更合理的開發者工具優化,惟一的幾個 Break Change 不會對項目帶來實質影響,期待正式版的發佈。

討論地址是:精讀《Typescript 4》· Issue #259 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索