TypeScript 高級用法

hi,豆皮粉兒們,今天又和你們見面了,本期分享的是由bytedancer「米蘭的小鐵匠」, 帶來的TypeScript高級使用, 適用於對TypeScript已經有所瞭解或者已經實際用過一段時間的同窗, 本文分別從類型、運算符、操做符、泛型的角度來系統介紹常見的TypeScript文章沒有好好講解的功能點, 最後再分享一下做者的實踐經歷。css

5c6e7105-97d1-46b5-a71f-5958494f9332.gif

做者:米蘭的小鐵匠html

1、 類型

unknown

unknown指的是不可預先定義的類型,在不少場景下,它能夠替代any的功能同時保留靜態檢查的能力。前端

const num: number = 10;
(num as unknown as string).split('');          // 注意,這裏和any同樣徹底能夠經過靜態檢查
複製代碼

這個時候unknown的做用就跟any高度相似了,你能夠把它轉化成任何類型,不一樣的地方是,在靜態編譯的時候,unknown不能調用任何方法,而any能夠。git

const foo: unknown = 'string';
foo.substr(1);           // Error: 靜態檢查不經過報錯
const bar: any = 10;
bar.substr(1);                // Pass: any類型至關於放棄了靜態檢查
複製代碼

unknown的一個使用場景是,避免使用any做爲函數的參數類型而致使的靜態類型檢查bug:github

function test(input: unknown): number {
  if (Array.isArray(input)) {
    return input.length;    // Pass: 這個代碼塊中,類型守衛已經將input識別爲array類型
  }
  return input.length;      // Error: 這裏的input仍是unknown類型,靜態檢查報錯。若是入參是any,則會放棄檢查直接成功,帶來報錯風險
}
複製代碼

void

在TS中,void和undefined功能高度相似,能夠在邏輯上避免不當心使用了空指針致使的錯誤web

function foo() {}          // 這個空函數沒有返回任何值,返回類型缺省爲void
const a = foo();        // 此時a的類型定義爲void,你也不能調用a的任何屬性方法
複製代碼

void和undefined類型最大的區別是,你能夠理解爲undefined是void的一個子集,當你對函數返回值並不在乎時,使用void而不是undefined。舉一個React中的實際的例子。typescript

// Parent.tsx
function Parent(): JSX.Element {
  const getValue = (): number => { return 2 };           /* 這裏函數返回的是number類型 */
  // const getValue = (): string => { return 'str' }; /* 這裏函數返回的string類型,一樣能夠傳給子屬性 */
  return <Child getValue={getValue} />
}
複製代碼
// Child.tsx
type Props = {
  getValue: () => void;  // 這裏的void表示邏輯上不關注具體的返回值類型,number、string、undefined等均可以
}
const Child = ({ getValue }: Props) => <div onClick={() => getValue()}>click</div>;
複製代碼

never

never是指無法正常結束返回的類型,一個一定會報錯或者死循環的函數會返回這樣的類型。編程

function foo(): never { throw new Error('error message') }  // throw error 返回值是never
function foo(): never { while(true){} }  // 這個死循環的也會沒法正常退出
function foo(): never { let count = 1; while(count){ count ++; } }  // Error: 這個沒法將返回值定義爲never,由於沒法在靜態編譯階段直接識別出
複製代碼

還有就是永遠沒有相交的類型後端

type human = 'boy' & 'girl' // 這兩個單獨的字符串類型並不可能相交,故human爲never類型
複製代碼

不過任何類型聯合上 never類型,仍是原來的類型前端工程化

type language = 'ts' | never   // language的類型仍是'ts'類型
複製代碼

關於never有以下特性:

  • 在一個函數中調用了返回never的函數後,以後的代碼都會變成deadcode
function test() {
  foo();                  // 這裏的foo指上面返回never的函數
  console.log(111);         // Error: 編譯器報錯,此行代碼永遠不會執行到
}
複製代碼
  • 沒法把其餘類型賦給never
let n: never;
const o: any = {};
n = o;  // Error: 不能把一個非never類型賦值給never類型,包括any
複製代碼

關於never的這個特性有一些很hack的用法和討論,好比這個知乎下的 尤雨溪的回答

2、運算符

非空斷言運算符 !

這個運算符能夠用在變量名或者函數名以後,用來強調對應的元素是非null|undefined的

function onClick(callback?: () => void) {
  callback!();                // 參數是可選入參,加了這個感嘆號!以後,TS編譯不報錯
}
複製代碼

查看編譯後的ES5代碼,竟然沒有作任何防空判斷

function onClick(callback) {
  callback();
}
複製代碼

這個符號的場景,特別適用於咱們已經明確知道不會返回空值的場景,從而減小冗餘的代碼判斷,如React的Ref

function Demo(): JSX.Elememt {
  const divRef = useRef<HTMLDivElement>();
  useEffect(() => {
    divRef.current!.scrollIntoView();         // 當組件Mount後纔會觸發useEffect,故current必定是有值的
  }, []);
  return <div ref={divRef}>Demo</div>
}
複製代碼

可選鏈運算符 ?.

相比上面!做用於編譯階段的非空判斷,?.這個是開發者最須要的運行時(固然編譯時也有效)的非空判斷

obj?.prop    obj?.[index]    func?.(args)
複製代碼

?.用來判斷左側的表達式是不是null | undefined,若是是則會中止表達式運行,能夠減小咱們大量的&&運算

好比咱們寫出a?.b時,編譯器會自動生成以下代碼

a === null || a === void 0 ? void 0 : a.b;
複製代碼

這裏涉及到一個小知識點: undefined這個值在非嚴格模式下會被從新賦值,使用void 0一定返回真正的undefined

空值合併運算符 ??

??與||的功能是類似的,區別在於**??在左側表達式結果爲null或者undefined時,纔會返回右側表達式**

好比咱們書寫了const b = a ?? 10,生成的代碼以下

const b = a !== null && a !== void 0 ? a : 10;
複製代碼

而 || 表達式,你們知道的,則對false、''、NaN、0等邏輯空值也會生效,不適於咱們作對參數的合併

數字分隔符_

const num:number = 1_2_345.6_78_9
複製代碼

_能夠用來對長數字作任意的分隔,主要設計是爲了便於數字的閱讀,編譯出來的代碼是沒有下劃線的,請放心食用

3、操做符

鍵值獲取 keyof

keyof能夠獲取一個類型全部鍵值,返回一個聯合類型,以下

type Person = {
  name: string;
  age: number;
}
type PersonKey = keyof Person;  // PersonKey獲得的類型爲 'name' | 'age' 
複製代碼

keyof的一個典型用途是限制訪問對象的key合法化,由於any作索引是不被接受的

function getValue (p: Person, k: keyof Person) {
  return p[k];  // 若是k不如此定義,則沒法以p[k]的代碼格式經過編譯
}
複製代碼

總結起來keyof的語法格式以下

類型 = keyof 類型
複製代碼

實例類型獲取 typeof

typeof 是獲取一個對象/實例的類型,以下

const me: Person = { name: 'gzx', age: 16 };
type P = typeof me;  // { name: string, age: number | undefined }
const you: typeof me = { name: 'mabaoguo', age: 69 }  // 能夠經過編譯
複製代碼

typeof 只能用在具體的對象上,這與js中的typeof是一致的,而且它會根據左側值自動決定應該執行哪一種行爲

const typestr = typeof me;   // typestr的值爲"object"
複製代碼

typeof 能夠和keyof一塊兒使用(由於typeof是返回一個類型嘛),以下

type PersonKey = keyof typeof me;   // 'name' | 'age'
複製代碼

總結起來typeof的語法格式以下

類型 = typeof 實例對象
複製代碼

遍歷屬性 in

in只能用在類型的定義中,能夠對枚舉類型進行遍歷,以下

// 這個類型能夠將任何類型的鍵值轉化成number類型
type TypeToNumber<T> = {
  [key in keyof T]: number
} 
複製代碼

keyof返回泛型T的全部鍵枚舉類型,key是自定義的任何變量名,中間用in連接,外圍用[]包裹起來(這個是固定搭配),冒號右側number將全部的key定義爲number類型。

因而能夠這樣使用了

const obj: TypeToNumber<Person> = { name: 10, age: 10 }
複製代碼

總結起來in的語法格式以下

[ 自定義變量名 in 枚舉類型 ]: 類型
複製代碼

4、泛型

泛型在TS中能夠說是一個很是重要的屬性,它承載了從靜態定義到動態調用的橋樑,同時也是TS對本身類型定義的元編程。泛型能夠說是TS類型工具的精髓所在,也是整個TS最難學習的部分,這裏專門分兩章總結一下。

基本使用

泛型能夠用在普通類型定義,類定義、函數定義上,以下

// 普通類型定義
type Dog<T> = { name: string, type: T }
// 普通類型使用
const dog: Dog<number> = { name: 'ww', type: 20 }

// 類定義
class Cat<T> {
  private type: T;
  constructor(type: T) { this.type = type; }
}
// 類使用
const cat: Cat<number> = new Cat<number>(20); // 或簡寫 const cat = new Cat(20)

// 函數定義
function swipe<T, U>(value: [T, U]): [U, T] {
  return [value[1], value[0]];
}
// 函數使用
swipe<Cat<number>, Dog<number>>([cat, dog])  // 或簡寫 swipe([cat, dog])
複製代碼

注意,若是對一個類型名定義了泛型,那麼使用此類型名的時候必定要把泛型類型也寫上去。

而對於變量來講,它的類型能夠在調用時推斷出來的話,就能夠省略泛型書寫。

泛型的語法格式簡單總結以下

類型名<泛型列表> 具體類型定義
複製代碼

泛型推導與默認值

上面提到了,咱們能夠簡化對泛型類型定義的書寫,由於TS會自動根據變量定義時的類型推導出變量類型,這通常是發生在函數調用的場合的

type Dog<T> = { name: string, type: T }

function adopt<T>(dog: Dog<T>) { return dog };

const dog = { name: 'ww', type: 'hsq' };  // 這裏按照Dog類型的定義一個type爲string的對象
adopt(dog);  // Pass: 函數會根據入參類型推斷出type爲string
複製代碼

若不適用函數泛型推導,咱們若須要定義變量類型則必須指定泛型類型

const dog: Dog<string> = { name: 'ww', type: 'hsq' }  // 不可省略<string>這部分
複製代碼

若是咱們想不指定,可使用泛型默認值的方案

type Dog<T = any> = { name: string, type: T }
const dog: Dog = { name: 'ww', type: 'hsq' }
dog.type = 123;    // 不過這樣type類型就是any了,沒法自動推導出來,失去了泛型的意義
複製代碼

泛型默認值的語法格式簡單總結以下

泛型名 = 默認類型
複製代碼

泛型約束

有的時候,咱們能夠不用關注泛型具體的類型,如

function fill<T>(length: number, value: T): T[] {
  return new Array(length).fill(value);
}
複製代碼

這個函數接受一個長度參數和默認值,結果就是生成使用默認值填充好對應個數的數組。咱們不用對傳入的參數作判斷,直接填充就好了,可是有時候,咱們須要限定類型,這時候使用extends關鍵字便可

function sum<T extends number>(value: T[]): number {
  let count = 0;
  value.forEach(v => {count += v});
  return count;
}
複製代碼

這樣你就能夠以sum([1,2,3])這種方式調用求和函數,而像sum(['1', '2'])這種是沒法經過編譯的

泛型約束也能夠用在多個泛型參數的狀況

function pick<T, U extends keyof T>(){};
複製代碼

這裏的意思是限制了 U 必定是 T 的key類型中的子集,這種用法經常出如今一些泛型工具庫中。

extends的語法格式簡單總結以下,注意下面的類型既能夠是通常意義上的類型也能夠是泛型

泛型名 extends 類型
複製代碼

泛型條件

上面提到extends,其實也能夠當作一個三元運算符,以下

T extends U? X: Y
複製代碼

這裏便不限制T必定要是U的子類型,若是是U子類型,則將T定義爲X類型,不然定義爲Y類型。

注意,生成的結果是分配式的

舉個例子,若是咱們把X換成T,如此形式:T extends U? T: never

此時返回的T,是知足原來的T中包含U的部分,能夠理解爲T和U的交集

因此,extends的語法格式能夠擴展爲

泛型名A extends 類型B ? 類型C: 類型D
複製代碼

泛型推斷 infer

infer的中文是「推斷」的意思,通常是搭配上面的泛型條件語句使用的,所謂推斷,就是你不用預先指定在泛型列表中,在運行時會自動判斷,不過你得先預約義好總體的結構。舉個例子

type Foo<T> = T extends {t: infer Test} ? Test: string
複製代碼

首選看extends後面的內容,{t: infer Test}能夠當作是一個包含t屬性類型定義,這個t屬性的value類型經過infer進行推斷後會賦值給Test類型,若是泛型實際參數符合{t: infer Test}的定義那麼返回的就是Test類型,不然默認給缺省的string類型。

舉個例子加深下理解

type One = Foo<number>  // string,由於number不是一個包含t的對象類型
type Two = Foo<{t: boolean}>  // boolean,由於泛型參數匹配上了,使用了infer對應的type
type Three = Foo<{a: number, t: () => void}> // () => void,泛型定義是參數的子集,一樣適配
複製代碼

infer用來對知足的泛型類型進行子類型的抽取,有不少高級的泛型工具也巧妙的使用了這個方法。

5、泛型工具

Partical<T>

此工具的做用就是將泛型中所有屬性變爲可選的

type Partial<T> = {
        [key in keyof T]?: T[P]
}
複製代碼

舉個例子,這個類型定義在下面也會用到

type Animal = {
  name: string,
  category: string,
  age: number,
  eat: () => number
}
複製代碼

使用Partical包裹一下

type PartOfAnimal = Partical<Animal>;
const ww: PartOfAnimal = { name: 'ww' }; // 屬性所有可選後,能夠只賦值部分屬性了
複製代碼

Record<K, T>

此工具的做用是將K中全部屬性值轉化爲T類型,咱們經常使用它來申明一個普通object對象

type Record<K extends keyof any,T> = {
  [key in K]: T
}
複製代碼

這裏特別說明一下,keyof any對應的類型爲number | string | symbol,也就是能夠作對象鍵(專業說法叫索引index)的類型集合。

舉個例子

const obj: Record<string, string> = { 'name': 'xiaoming', 'tag': '三好學生' }
複製代碼

Pick<T, K>

此工具的做用是將T類型中的K鍵列表提取出來,生成新的子鍵值對類型

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]          
}
複製代碼

咱們仍是用上面的Animal定義,看一下Pick如何使用

const bird: Pick<Animal, "name" | "age"> = { name: 'bird', age: 1 }
複製代碼

Exclude<T, U>

此工具是在T類型中,去除T類型和U類型的交集,返回剩餘的部分

type Exclude<T, U> = T extends U ? never : T
複製代碼

注意這裏的extends返回的T是原來的T中和U無交集的屬性,而任何屬性聯合never都是自身,具體可在上文查閱。

舉個例子

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;   // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
複製代碼

Omit<T, K>

此工具可認爲是適用於鍵值對對象的Exclude,它會去除類型T中包含K的鍵值對

type Omit = Pick<T, Exclude<keyof T, K>>
複製代碼

在定義中,第一步先從T的key中去掉與K重疊的key,接着使用Pick把T類型和剩餘的key組合起來便可

仍是用上面的Animal舉個例子

const OmitAnimal:Omit<Animal, 'name'|'age'> = { category: 'lion', eat: () => { console.log('eat') } }
複製代碼

能夠發現,Omit與Pick獲得的結果徹底相反,一個是取非結果,一個取交結果。

ReturnType<T>

此工具就是獲取T類型(函數)對應的返回值類型

type ReturnType<T extends (...args: any) => any> 
  = T extends (...args: any) => infer R ? R : any;
複製代碼

看源碼其實有點多,其實能夠稍微簡化成下面的樣子

type ReturnType<T extends func> = T extends () => infer R ? R: any;
複製代碼

經過使用infer推斷返回值類型,而後返回此類型,若是你完全理解了infer的含義,那這段就很好理解

舉個例子

function foo(x: string | number): string | number { /*..*/ }
type FooType = ReturnType<foo>;  // string | number
複製代碼

Required<T>

此工具能夠將類型T中全部的屬性變爲必選項

type Required<T> = {
  [P in keyof T]-?: T[P]
}
複製代碼

這裏有一個頗有意思的語法-?,你能夠理解爲就是TS中把?可選屬性減去的意思。

除了這些之外,還有不少的內置的類型工具,能夠參考TypeScript Handbook得到更詳細的信息,同時Github上也有不少第三方類型輔助工具,如utility-types等。

6、項目實戰

這裏分享一些我我的的想法,可能也許會比較片面甚至錯誤,歡迎你們積極留言討論

Q: 偏好使用interface仍是type來定義類型?

A: 從用法上來講二者本質上沒有區別,你們使用React項目作業務開發的話,主要就是用來定義Props以及接口數據類型。

可是從擴展的角度來講,type比interface更方便拓展一些,假若有如下兩個定義

type Name = { name: string };
interface IName { name: string };
複製代碼

想要作類型的擴展的話,type只須要一個&,而interface要多寫很多代碼

type Person = Name & { age: number };
interface IPerson extends IName { age: number };
複製代碼

另外type有一些interface作不到的事情,好比使用|進行枚舉類型的組合,使用typeof獲取定義的類型等等。

不過interface有一個比較強大的地方就是能夠重複定義添加屬性,好比咱們須要給window對象添加一個自定義的屬性或者方法,那麼咱們直接基於其Interface新增屬性就能夠了。

declare global {
    interface Window { MyNamespace: any; }
}
複製代碼

整體來講,你們知道TS是類型兼容而不是類型名稱匹配的,因此通常不需用面向對象的場景或者不須要修改全局類型的場合,我通常都是用type來定義類型。

Q: 是否容許any類型的出現

A: 說實話,剛開始使用TS的時候仍是挺喜歡用any的,畢竟你們都是從JS過渡過來的,對這種影響效率的代碼開發方式並不能徹底接受,所以無論是出於偷懶仍是找不到合適定義的狀況,使用any的狀況都比較多。

隨着使用時間的增長和對TS學習理解的加深,逐步離不開了TS帶來的類型定義紅利,不但願代碼中出現any,全部類型都必需要一個一個找到對應的定義,甚至已經喪失了裸寫JS的勇氣。

這是一個目前沒有正確答案的問題,老是要在效率和時間等等因素中找一個最適合本身的平衡。不過我仍是推薦使用TS,隨着前端工程化演進和地位的提升,強類型語言必定是多人協做和代碼健壯最可靠的保障之一,多用TS,少用any,也是前端界的一個廣泛共識。

Q: 類型定義文件(.d.ts)如何放置

A: 這個好像業界也沒有特別統一的規範,個人想法以下:

  • 臨時的類型,直接在使用時定義

如本身寫了一個組件內部的Helper,函數的入參和出參只供內部使用也不存在複用的可能,能夠直接在定義函數的時候就在後面定義

function format(input: {k: string}[]): number[] { /***/ }
複製代碼
  • 組件個性化類型,直接定義在ts(x)文件中

如AntD組件設計,每一個單獨組件的Props、State等專門定義了類型並export出去

// Table.tsx
export type TableProps = { /***/ }
export type ColumnProps = { /***/ }
export default function Table() { /***/ }
複製代碼

這樣使用者若是須要這些類型能夠經過import type的方式引入來使用。

  • 範圍/全局數據,定義在.d.ts文件中

全局類型數據,這個你們毫無異議,通常根目錄下有個typings文件夾,裏面會存放一些全局類型定義。

假如咱們使用了css module,那麼咱們須要讓TS識別.less文件(或者.scss)引入後是一個對象,能夠如此定義

declare module '*.less' {
  const resource: { [key: string]: string };
  export = resource;
}
複製代碼

而對於一些全局的數據類型,如後端返回的通用的數據類型,我也習慣將其放在typings文件夾下,使用Namespace的方式來避免名字衝突,如此能夠節省組件import類型定義的語句

declare namespace EdgeApi {
  export interface Department {
    description: string;
    gmt_create: string;
    gmt_modify: string;
    id: number;
    name: string;
  }
}
複製代碼

這樣,每次使用的時候,只須要const department: EdgeApi.Department便可,節省了很多導入的精力。開發者只要能約定規範,避免命名衝突便可。

關於TS用法的總結就結束到這裏,感謝你們的觀看~

數據平臺前端團隊,在公司內負責風神、TEA、Libra、Dorado等大數據相關產品的研發。咱們在前端技術上保持着很是強的熱情,除了數據產品相關的研發外,在數據可視化、海量數據處理優化、web excel、WebIDE、私有化部署、工程工具都方面都有不少的探索和積累,有興趣能夠與咱們聯繫。

相關文章
相關標籤/搜索