前言
最近,我們部門在開發(fā)一個組件庫時,我注意到一些團(tuán)隊成員對使用TypeScript表示出了抵觸情緒,他們常常抱怨說:“TypeScript太麻煩了,我們不想用!”起初,我對此感到困惑:TypeScript真的有那么麻煩嗎?然而,當(dāng)我抽時間審查隊伍的代碼時,我終于發(fā)現(xiàn)了問題所在。在這篇文章中,我想和大家分享我的一些發(fā)現(xiàn)和解決方案。
一、類型復(fù)用不足
在代碼審查過程中,我發(fā)現(xiàn)了大量的重復(fù)類型定義,這顯著降低了代碼的復(fù)用性。
進(jìn)一步交流后,我了解到許多團(tuán)隊成員并不清楚如何在TypeScript中復(fù)用類型。TypeScript允許我們使用type
和interface
來定義類型。
當(dāng)我詢問他們type
與interface
之間的區(qū)別時,大多數(shù)人都表示不清楚,這也就難怪他們不知道如何有效地復(fù)用類型了。
type
定義的類型可以通過交叉類型(&
)來進(jìn)行復(fù)用,而interface
定義的類型則可以通過繼承(extends
)來實現(xiàn)復(fù)用。值得注意的是,type
和interface
定義的類型也可以互相復(fù)用。下面是一些簡單的示例:
復(fù)用type
定義的類型:
type Point = {
x: number;
y: number;
};
type Coordinate = Point & {
z: number;
};
復(fù)用interface
定義的類型:
interface Point {
x: number;
y: number;
};
interface Coordinate extends Point {
z: number;
}
interface
復(fù)用type
定義的類型:
type Point = {
x: number;
y: number;
};
interface Coordinate extends Point {
z: number;
}
type
復(fù)用interface
定義的類型:
interface Point {
x: number;
y: number;
};
type Coordinate = Point & {
z: number;
};
二、復(fù)用時只會新增屬性的定義
我還注意到,在類型復(fù)用時,團(tuán)隊成員往往只是簡單地為已有類型新增屬性,而忽略了更高效的復(fù)用方式。
例如,有一個已有的類型Props
需要復(fù)用,但不需要其中的屬性c
。在這種情況下,團(tuán)隊成員會重新定義Props1
,僅包含Props
中的屬性a
和b
,同時添加新屬性e
。
interface Props {
a: string;
b: string;
c: string;
}
interface Props1 {
a: string;
b: string;
e: string;
}
實際上,我們可以利用TypeScript提供的工具類型Omit
來更高效地實現(xiàn)這種復(fù)用。
interface Props {
a: string;
b: string;
c: string;
}
interface Props1 extends Omit<Props, 'c'> {
e: string;
}
類似地,工具類型Pick
也可以用于實現(xiàn)此類復(fù)用。
interface Props {
a: string;
b: string;
c: string;
}
interface Props1 extends Pick<Props, 'a' | 'b'> {
e: string;
}
Omit
和Pick
分別用于排除和選擇類型中的屬性,具體使用哪一個取決于具體需求。
三、未統(tǒng)一使用組件庫的基礎(chǔ)類型
在開發(fā)組件庫時,我們經(jīng)常面臨相似功能組件屬性命名不一致的問題,這不僅影響了組件庫的易用性,也降低了其可維護(hù)性。
為了解決這一問題,定義一套統(tǒng)一的基礎(chǔ)類型至關(guān)重要。這套基礎(chǔ)類型為組件庫的開發(fā)提供了堅實的基礎(chǔ),確保了所有組件在命名上的一致性。
以表單控件為例,我們可以定義如下基礎(chǔ)類型:
import { CSSProperties } from 'react';
type Size = 'small' | 'middle' | 'large';
interface BaseProps<T> {
className?: string;
style?: CSSProperties;
visible?: boolean;
size?: Size;
disabled?: boolean;
readOnly?: boolean;
defaultValue?: T;
value?: T;
onChange: (value: T) => void;
}
基于這些基礎(chǔ)類型,定義具體組件的屬性類型變得簡單而直接:
interface WInputProps extends BaseProps<string> {
maxLength?: number;
showCount?: boolean;
}
通過使用type
關(guān)鍵字定義基礎(chǔ)類型,我們可以避免類型被意外修改,進(jìn)而增強代碼的穩(wěn)定性和可維護(hù)性。
四、處理含有不同類型元素的數(shù)組
在審查自定義Hook時,我發(fā)現(xiàn)團(tuán)隊成員傾向于返回對象,即使Hook只返回兩個值。
雖然這樣做并非錯誤,但它違背了自定義Hook的一個常見規(guī)范:當(dāng)Hook返回兩個值時,應(yīng)使用數(shù)組返回。
團(tuán)隊成員解釋說,他們不知道如何定義含有不同類型元素的數(shù)組,通常會選擇使用any[]
,但這會帶來類型安全問題,因此他們選擇返回對象。
實際上,元組是處理這種情況的理想選擇。通過元組,我們可以在一個數(shù)組中包含不同類型的元素,同時保持每個元素類型的明確性。
function useMyHook(): [string, number] {
return ['示例文本', 42];
}
function MyComponent() {
const [text, number] = useMyHook();
console.log(text); // 輸出字符串
console.log(number); // 輸出數(shù)字
return null;
}
在這個例子中,useMyHook
函數(shù)返回一個明確類型的元組,包含一個string
和一個number
。在MyComponent
組件中使用這個Hook時,我們可以通過解構(gòu)賦值來獲取這兩個不同類型的值,同時保持類型安全。
五、處理參數(shù)數(shù)量和類型不固定的函數(shù)
審查團(tuán)隊成員封裝的函數(shù)時,我發(fā)現(xiàn)當(dāng)函數(shù)的參數(shù)數(shù)量不固定、類型不同或返回值類型不同時,他們傾向于使用any
定義參數(shù)和返回值。
他們解釋說,他們只知道如何定義參數(shù)數(shù)量固定、類型相同的函數(shù),對于復(fù)雜情況則不知所措,而且不愿意將函數(shù)拆分為多個函數(shù)。
這正是函數(shù)重載發(fā)揮作用的場景。通過函數(shù)重載,我們可以在同一函數(shù)名下定義多個函數(shù)實現(xiàn),根據(jù)不同的參數(shù)類型、數(shù)量或返回類型進(jìn)行區(qū)分。
function greet(name: string): string;
function greet(age: number): string;
function greet(value: any): string {
if (typeof value === "string") {
return `Hello, ${value}`;
} else if (typeof value === "number") {
return `You are ${value} years old`;
}
}
在這個例子中,我們?yōu)?code style="margin: 0px 2px;padding: 2px 4px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;border-radius: 4px;background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;color: rgb(239, 112, 96)">greet函數(shù)提供了兩種調(diào)用方式,使得函數(shù)使用更加靈活,同時保持類型安全。
對于箭頭函數(shù),雖然它們不直接支持函數(shù)重載,但我們可以通過定義函數(shù)簽名的方式來實現(xiàn)類似的效果。
e GreetFunction = {
(name: string): string;
(age: number): string;
};
const greet: GreetFunction = (value: any): string => {
if (typeof value === "string") {
return `Hello, ${value}`;
} else if (typeof value === "number") {
return `You are ${value} years old.`;
}
return '';
};
這種方法利用了類型系統(tǒng)來提供編譯時的類型檢查,模擬了函數(shù)重載的效果。
六、組件屬性定義:使用type還是interface?
在審查代碼時,我發(fā)現(xiàn)團(tuán)隊成員在定義組件屬性時既使用type
也使用interface
。
詢問原因時,他們表示兩者都可以用于定義組件屬性,沒有明顯區(qū)別。
由于同名接口會自動合并,而同名類型別名會沖突,我推薦使用interface
定義組件屬性。這樣,使用者可以通過declare module
語句自由擴展組件屬性,增強了代碼的靈活性和可擴展性。
interface UserInfo {
name: string;
}
interface UserInfo {
age: number;
}
const userInfo: UserInfo = { name: "張三", age: 23 };
結(jié)語
TypeScript的使用并不困難,關(guān)鍵在于理解和應(yīng)用其提供的強大功能。如果你在使用TypeScript過程中遇到任何問題,不清楚應(yīng)該使用哪種語法或技巧來解決,歡迎在評論區(qū)留言。我們一起探討,共同解決TypeScript中遇到的挑戰(zhàn)。
該文章在 2024/3/21 15:43:04 編輯過