hi,豆皮粉兒們,今天又和你們見面了,本期分享的是由bytedancer「米蘭的小鐵匠」, 帶來的TypeScript高級使用, 適用於對TypeScript已經有所瞭解或者已經實際用過一段時間的同窗, 本文分別從類型、運算符、操做符、泛型的角度來系統介紹常見的TypeScript文章沒有好好講解的功能點, 最後再分享一下做者的實踐經歷。css
做者:米蘭的小鐵匠html
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是指無法正常結束返回的類型,一個一定會報錯或者死循環的函數會返回這樣的類型。編程
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有以下特性:
deadcode
function test() {
foo(); // 這裏的foo指上面返回never的函數
console.log(111); // Error: 編譯器報錯,此行代碼永遠不會執行到
}
複製代碼
let n: never;
const o: any = {};
n = o; // Error: 不能把一個非never類型賦值給never類型,包括any
複製代碼
關於never的這個特性有一些很hack的用法和討論,好比這個知乎下的 尤雨溪的回答
這個運算符能夠用在變量名或者函數名以後,用來強調對應的元素是非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
複製代碼
_能夠用來對長數字作任意的分隔,主要設計是爲了便於數字的閱讀,編譯出來的代碼是沒有下劃線的,請放心食用
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 是獲取一個對象/實例的類型,以下
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只能用在類型的定義中,能夠對枚舉類型進行遍歷,以下
// 這個類型能夠將任何類型的鍵值轉化成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 枚舉類型 ]: 類型
複製代碼
泛型在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的中文是「推斷」的意思,通常是搭配上面的泛型條件語句使用的,所謂推斷,就是你不用預先指定在泛型列表中,在運行時會自動判斷,不過你得先預約義好總體的結構。舉個例子
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
用來對知足的泛型類型進行子類型的抽取,有不少高級的泛型工具也巧妙的使用了這個方法。
此工具的做用就是將泛型中所有屬性變爲可選的
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' }; // 屬性所有可選後,能夠只賦值部分屬性了
複製代碼
此工具的做用是將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': '三好學生' }
複製代碼
此工具的做用是將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 }
複製代碼
此工具是在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
複製代碼
此工具可認爲是適用於鍵值對對象的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獲得的結果徹底相反,一個是取非結果,一個取交結果。
此工具就是獲取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
複製代碼
此工具能夠將類型T中全部的屬性變爲必選項
type Required<T> = {
[P in keyof T]-?: T[P]
}
複製代碼
這裏有一個頗有意思的語法-?
,你能夠理解爲就是TS中把?可選屬性減去的意思。
除了這些之外,還有不少的內置的類型工具,能夠參考TypeScript Handbook得到更詳細的信息,同時Github上也有不少第三方類型輔助工具,如utility-types等。
這裏分享一些我我的的想法,可能也許會比較片面甚至錯誤,歡迎你們積極留言討論
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來定義類型。
A: 說實話,剛開始使用TS的時候仍是挺喜歡用any的,畢竟你們都是從JS過渡過來的,對這種影響效率的代碼開發方式並不能徹底接受,所以無論是出於偷懶仍是找不到合適定義的狀況,使用any的狀況都比較多。
隨着使用時間的增長和對TS學習理解的加深,逐步離不開了TS帶來的類型定義紅利,不但願代碼中出現any,全部類型都必需要一個一個找到對應的定義,甚至已經喪失了裸寫JS的勇氣。
這是一個目前沒有正確答案的問題,老是要在效率和時間等等因素中找一個最適合本身的平衡。不過我仍是推薦使用TS,隨着前端工程化演進和地位的提升,強類型語言必定是多人協做和代碼健壯最可靠的保障之一,多用TS,少用any,也是前端界的一個廣泛共識。
A: 這個好像業界也沒有特別統一的規範,個人想法以下:
如本身寫了一個組件內部的Helper,函數的入參和出參只供內部使用也不存在複用的可能,能夠直接在定義函數的時候就在後面定義
function format(input: {k: string}[]): number[] { /***/ }
複製代碼
如AntD組件設計,每一個單獨組件的Props、State等專門定義了類型並export出去
// Table.tsx
export type TableProps = { /***/ }
export type ColumnProps = { /***/ }
export default function Table() { /***/ }
複製代碼
這樣使用者若是須要這些類型能夠經過import type的方式引入來使用。
全局類型數據,這個你們毫無異議,通常根目錄下有個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、私有化部署、工程工具都方面都有不少的探索和積累,有興趣能夠與咱們聯繫。