關于 TypeScript 一些高級用法的總結

前言
TypeScript 是 JavaScript 的類型的超集,它可以編譯成純 JavaScript。編譯出來的 JavaScript 可以運行在任何瀏覽器上。TypeScript 編譯工具可以運行在任何服務器和任何系統上。筆者使用typescript也差不多快一年了,使用Ts也感受頗深,Ts無疑增強我們代碼的規(guī)范性和可維護性,但是也同時增加了我們的開發(fā)負擔,但是其實對一個項目的長期而言,我認為這是值得的,這篇文章我們來講一下Ts的高級用法,適合剛接觸Ts的同學但是學的沒有那么深的同學,至于我們的vue源碼分析,我先暫更一周,在以后的時間將會繼續(xù)講到。
一、類型
1. unknow
unknown 指的是不可預先定義的類型,在很多場景下,它可以替代 any 的功能同時保留靜態(tài)檢查的能力。
const num: number = 10;(num as string).split('') // error 類型 "number" 到類型 "string" 的轉換可能是錯誤的,因為兩種類型不能充分重疊。如果這是有意的,請先將表達式轉換為 "unknown"。(num as unknown as string).split('')
這個時候 unknown 的作用就跟 any 高度類似了,你可以把它轉化成任何類型,不同的地方是,在靜態(tài)編譯的時候,unknown 不能調用任何方法,而 any 可以。
const foo: unknown = 'string';foo.substr(1); // Error: 靜態(tài)檢查不通過報錯const bar: any = 10;bar.substr(1); // Pass: any類型相當于放棄了靜態(tài)檢查
unknown 的一個使用場景是,避免使用 any 作為函數的參數類型而導致的靜態(tài)類型檢查 bug:
function test(input: unknown): number {if (Array.isArray(input)) {return input.length; // Pass: 這個代碼塊中,類型守衛(wèi)已經將input識別為array類型}return input.length; // Error: 這里的input還是unknown類型,靜態(tài)檢查報錯。如果入參是any,則會放棄檢查直接成功,帶來報錯風險}
2. enum
枚舉的話可以增強我們代碼的可讀性,例如我在業(yè)務中需要個后端傳一個回答問題的類型type,回復文本的話需要傳一個1,回復圖片的會傳2,如果直接在調用接口的時候傳一個2那肯定會讓別人看上去很疑惑,如此一來我們可以定義一個枚舉,需要傳值例如傳一個視頻,我們可以傳AnswerMessageType.video,而且在寫接口規(guī)范參數類型的時候我們可以規(guī)定這個type為AnswerMessageType,如此一來你要是傳個7就肯定不能通過的。
/**回復問題的消息類型*/export enum AnswerMessageType {text = 0,pictrue = 1,video = 2,voice = 3,file = 4,/**Srceent Rock 鏈接*/srlink = 6}
3. void
在 TS 中,void 和 undefined 功能高度類似,可以在邏輯上避免不小心使用了空指針導致的錯誤。void 和 undefined 類型最大的區(qū)別是,你可以理解為 undefined 是 void 的一個子集,當你對函數返回值并不在意時,使用 void 而不是 undefined。舉一個 React 中的實際的例子。
// Parent.tsxconst Parent: FC = () => {const getValue = (): number => { return 2 }; /* 這里函數返回的是number類型 */// const getValue = (): string => { return 'str' }; /* 這里函數返回的string類型,同樣可以傳給子屬性 */return <Child getValue={getValue} />}// Child.tsxtype Props = {getValue: () => void; // 這里的void表示邏輯上不關注具體的返回值類型,number、string、undefined等都可以}function Child({ getValue }: Props) => <div>{getValue()}</div>
運算符
1. 非空斷言運算符 !
這個運算符可以用在變量名或者函數名之后,用來強調對應的元素是非 null|undefined 的
function onClick(callback?: () => void) {callback(); // 參數是可選入參,加了這個感嘆號!之后,TS編譯不報錯}
這個符號的場景,特別適用于我們已經明確知道不會返回空值的場景,從而減少冗余的代碼判斷,如 React 的 Ref。以下是筆者工作中寫的一個自定義hook,左右兩個div,左邊放圖片,右邊是一段文字文字的多少是不確定的,左邊的圖片根據右邊的盒子來計算大小,如下使用了多個ref,但是我們在一些情況寫可以!來判斷e.target!或者divRef.current!來進行一個書寫
export function useCalcImg() {const [maxWidth, setMaxWidth] = useState(0)const [maxHeight, setMaxHeight] = useState(0)const [imgWidth, setImgWidth] = useState(0)const [imgHeiht, setImgHeight] = useState(0)const imgRef = useRef(null)const divRef = useRef<HTMLDivElement>()const handleImgLoad = useCallback((e) => {let img = e.target as HTMLImageElementsetImgWidth(img.naturalWidth)setImgHeight(img.naturalHeight)}, [])const calc = () => {if (maxWidth && imgWidth && imgRef.current) {let div = divRef.currentsetTimeout(() => {let { width, height } = Util.calc.fixedImgScale(imgWidth, imgHeiht, div.clientWidth, div.clientHeight)imgRef.current.width = widthimgRef.current.height = heightremoveClass(imgRef.current, 'hidden')}, 100)imgRef.current.width = 100}}const imgRefCallback = useCallback((img) => {imgRef.current = img}, [])useEffect(() => {calc()}, [imgRef, divRef, maxWidth, imgHeiht])const refCallback = useCallback((e) => {divRef.current = eif (e) {const { clientWidth, clientHeight } = esetMaxWidth(clientWidth)setMaxHeight(clientHeight)}}, [])return {imgRefCallback,handleImgLoad,refCallback}}
2. 可選鏈操作運算符 ?
相比上面!作用于編譯階段的非空判斷,?.這個是開發(fā)者最需要的運行時(當然編譯時也有效)的非空判斷。
// 例如后端有個參數標識flag,有標識的時候傳flag: 1, 沒有標識的時候不傳data?.flag
?.用來判斷左側的表達式是否是 null | undefined,如果是則會停止表達式運行,可以減少我們大量的&&運算。
比如我們寫出a?.b時,編譯器會自動生成如下代碼
let b = a !== null && a !== void 0 ? a : 10;
三、操作符
1. keyof
keyof 可以獲取一個類型所有鍵值,返回一個聯合類型,如下:
type Person = {name: string;age: number;}type PersonKey = keyof Person; // PersonKey得到的類型為 'name' | 'age'
2. typeof
typeof 是獲取一個對象/實例的類型,如下
interface Person {name: string,age: number}const people: Person = {name: 'thl',age: 22}const P = typeof people // { name: string, age: number | undefined }const me: typeof people = { name: 'wsy', age: 21 }
3. in
in 只能用在類型的定義中,可以對枚舉類型進行遍歷,如下:
// 這個類型可以將任何類型的鍵值轉化成number類型type TypeToNumber<T> = {[key in keyof T]: number}
keyof返回泛型 T 的所有鍵枚舉類型,key是自定義的任何變量名,中間用in鏈接,外圍用[]包裹起來(這個是固定搭配),冒號右側number將所有的key定義為number類型。
const person: TypeToNumber<Person> = {name: 21, age: 21}// [ 自定義變量名 in 枚舉類型 ]: 類型
四、泛型
泛型在 TS 中可以說是一個非常重要的屬性,它承載了從靜態(tài)定義到動態(tài)調用的橋梁,同時也是 TS 對自己類型定義的元編程。泛型可以說是 TS 類型工具的精髓所在,也是整個 TS 最難學習的部分?;臼褂?/span>
// 普通類型定義type Person<T> = { name: string, type: T }// 普通類型使用const person: Person<number> = { name: 'ww', type: 20 }// 類定義class People<T> {private type: T;constructor(type: T) { this.type = type; }}// 類使用const people: People<number> = new People<number>(20); // 或簡寫 const people = new People(20)// 函數定義function swipe<T, U>(value: [T, U]): [U, T] {return [value[1], value[0]];}
例如深度克隆
export function deepCopy<T extends Record<any, any>>(data: T): T {const t = typeOf(data)let oif (t === 'array') {o = []} else if (t === 'object') {o = {}} else {return data}if (t === 'array') {for (let i = 0; i < ((data as any) as any[]).length; i++) {o.push(deepCopy(data[i]))}} else if (t === 'object') {for (let i in data) {o[i] = deepCopy(data[i])}}return o}
五、高級泛型
1. Partial
此工具的作用就是將泛型中全部屬性變?yōu)榭蛇x的。
type Partial<T> = {[P in keyof T]?: T[P]}
舉個栗子,此時happy這個方法可以不傳
type Person = {name: stringage: numberhappy: () => void}const me: Partial<Person> = {name: 'thl',age: 22}
2、Record<K, T>
此工具的作用是將 K 中所有屬性值轉化為 T 類型,我們常用它來申明一個普通 object 對象。
type Record<K extends keyof any,T> = {[key in K]: T}
舉個栗子
const me: Record<string, string> = { 'name': 'thl', 'tag': '打工人' }
3. Pick<T, K>
此工具的作用是將 T 類型中的 K 鍵列表提取出來,生成新的子鍵值對類型。
type Pick<T, K extends keyof T> = {[P in K]: T[P]}
舉個栗子
interface Person {name: stringage: numberdo: () => void}type PeopleInfo = Pick<Person, 'name' | 'age'>// 將name和age兩個類型提取出來,不用從新定義類型const me: Person { name: 'thl', age: 22}
4: Exclude<T, U>
此工具是在 T 類型中,去除 T 類型和 U 類型的交集,返回剩余的部分。
type Exclude<T, U> = T extends U ? never : T
舉個栗子
type PersonOne = {name: stringage: numbersleep: () => void}type PersonTwo = {name: stringage: numbereat: () => void}type Person = Exclude<PersonOne, PersonTwo>等價于type PersonDo = {sleep: () => voideat: () => void}
5. Omit<T, K>
此工具可認為是適用于鍵值對對象的 Exclude,它會去除類型 T 中包含 K 的鍵值對。
type Omit = Pick<T, Exclude<keyof T, K
舉個栗子
type Person = Omit<PersonOne, 'age'>const me: Person = {name: 'thl',sleep: () => console.log('i will')}
6. ReturnType
此工具就是獲取 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;
舉個栗子
function foo(x: string | number): string | number {// Todo}type FooType = ReturnType<foo>; // string | number
7. Parameters
此工具就是獲取T類型(函數)對應的參數類型
type Parameters<T> = T extends (...args:string[]) => any ? string[] : any;
老規(guī)矩舉個栗子
type Fn = (a: string, b: number) => voidtype FnParams = Parameters<Fn>
其實還有一些高級類型這里就不一一列舉了,有興趣的可以去逛下官網www.typescriptlang.org
總結
Ts的使用有很多技巧,我們在日常使用中可能沒有那么容易遇到,在我們閱讀一些源碼的時候就會了解到很多,還有一個需要總結的問題是關于type和interface的區(qū)別和我們如何選擇的問題,本質上沒有什么區(qū)別, 從擴展的角度上來看,type比interface好些,使用&可以少些一些代碼
type Person = {name: stringage: number}// 擴展type NewPerson = Person & {do: () => void}interface People {happyStatus: trueeat: () => void}// 擴展interface NewPeople extends People {name: string}
另外 type 有一些 interface 做不到的事情,比如使用 | 進行枚舉類型的組合,使用typeof獲取定義的類型等等。
不過 interface 有一個比較強大的地方就是可以重復定義添加屬性,比如我們需要給window對象添加一個自定義的屬性或者方法,那么我們直接基于其 Interface 新增屬性就可以了。
declare global {interface Window { MyNamespace: any; }}
個人習慣一般我使用type多一些
栗鼠怪
轉自:https://juejin.cn/post/6983576636025208846
