讓你更好使用Typescript的11個(gè)技巧分享
學(xué)習(xí)Typescript通常是一個(gè)重新發(fā)現(xiàn)的過(guò)程。最初印象可能很有欺騙性:這不就是一種注釋Javascript 的方式嗎,這樣編譯器就能幫助我找到潛在的bug?
雖然這種說(shuō)法總體上是正確的,但隨著你的前進(jìn),會(huì)發(fā)現(xiàn)語(yǔ)言最不可思議的力量在于組成、推斷和操縱類(lèi)型。
本文將總結(jié)幾個(gè)技巧,幫助你充分發(fā)揮語(yǔ)言的潛力。
將類(lèi)型想象成集合
類(lèi)型是程序員日常概念,但很難簡(jiǎn)明地定義它。我發(fā)現(xiàn)用集合作為概念模型很有幫助。
例如,新的學(xué)習(xí)者發(fā)現(xiàn)Typescript組成類(lèi)型的方式是反直覺(jué)的。舉一個(gè)非常簡(jiǎn)單的例子:
type Measure = { radius: number };
type Style = { color: string };
// typed { radius: number; color: string }
type Circle = Measure & Style;
如果你將 & 操作符解釋為邏輯與,你的可能會(huì)認(rèn)為 Circle 是一個(gè)啞巴類(lèi)型,因?yàn)樗莾蓚€(gè)沒(méi)有任何重疊字段的類(lèi)型的結(jié)合。這不是 TypeScript 的工作方式。相反,將其想象成集合會(huì)更容易推導(dǎo)出正確的行為:
- 每種類(lèi)型都是值的集合
- 有些集合是無(wú)限的,如 string、object;有些是有限的,如 boolean、undefined,...
unknown是通用集合(包括所有值),而never是空集合(不包括任何值)Type Measure是一個(gè)集合,包含所有包含名為radius的 number 字段的對(duì)象。Style也是如此。&運(yùn)算符創(chuàng)建了交集:Measure & Style表示包含radius和color字段的對(duì)象的集合,這實(shí)際上是一個(gè)較小的集合,但具有更多常用字段。- 同樣,
|運(yùn)算符創(chuàng)建了并集:一個(gè)較大的集合,但可能具有較少的常用字段(如果兩個(gè)對(duì)象類(lèi)型組合在一起)
集合也有助于理解可分配性:只有當(dāng)值的類(lèi)型是目標(biāo)類(lèi)型的子集時(shí)才允許賦值:
type ShapeKind = 'rect' | 'circle'; let foo: string = getSomeString(); let shape: ShapeKind = 'rect'; // 不允許,因?yàn)樽址皇?ShapeKind 的子集。 shape = foo; // 允許,因?yàn)?ShapeKind 是字符串的子集。 foo = shape;
理解類(lèi)型聲明和類(lèi)型收窄
TypeScript 有一項(xiàng)非常強(qiáng)大的功能是基于控制流的自動(dòng)類(lèi)型收窄。這意味著在代碼位置的任何特定點(diǎn),變量都具有兩種類(lèi)型:聲明類(lèi)型和類(lèi)型收窄。
function foo(x: string | number) {
if (typeof x === 'string') {
// x 的類(lèi)型被縮小為字符串,所以.length是有效的
console.log(x.length);
// assignment respects declaration type, not narrowed type
x = 1;
console.log(x.length); // disallowed because x is now number
} else {
...
}
}
使用帶有區(qū)分的聯(lián)合類(lèi)型而不是可選字段
在定義一組多態(tài)類(lèi)型(如 Shape)時(shí),可以很容易地從以下開(kāi)始:
type Shape = {
kind: 'circle' | 'rect';
radius?: number;
width?: number;
height?: number;
}
function getArea(shape: Shape) {
return shape.kind === 'circle' ?
Math.PI * shape.radius! ** 2
: shape.width! * shape.height!;
}
需要使用非空斷言(在訪問(wèn) radius、width 和 height 字段時(shí)),因?yàn)?kind 與其他字段之間沒(méi)有建立關(guān)系。相反,區(qū)分聯(lián)合是一個(gè)更好的解決方案:
type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;
function getArea(shape: Shape) {
return shape.kind === 'circle' ?
Math.PI * shape.radius ** 2
: shape.width * shape.height;
}
類(lèi)型收窄已經(jīng)消除了強(qiáng)制轉(zhuǎn)換的需要。
使用類(lèi)型謂詞來(lái)避免類(lèi)型斷言
如果你正確使用 TypeScript,你應(yīng)該很少會(huì)發(fā)現(xiàn)自己使用顯式類(lèi)型斷言(例如 value as SomeType);但是,有時(shí)你仍然會(huì)有一種沖動(dòng),例如:
type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;
function isCircle(shape: Shape) {
return shape.kind === 'circle';
}
function isRect(shape: Shape) {
return shape.kind === 'rect';
}
const myShapes: Shape[] = getShapes();
// 錯(cuò)誤,因?yàn)閠ypescript不知道過(guò)濾的方式
const circles: Circle[] = myShapes.filter(isCircle);
// 你可能傾向于添加一個(gè)斷言
// const circles = myShapes.filter(isCircle) as Circle[];
一個(gè)更優(yōu)雅的解決方案是將isCircle和isRect改為返回類(lèi)型謂詞,這樣它們可以幫助Typescript在調(diào)用 filter 后進(jìn)一步縮小類(lèi)型。
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle';
}
function isRect(shape: Shape): shape is Rect {
return shape.kind === 'rect';
}
...
// now you get Circle[] type inferred correctly
const circles = myShapes.filter(isCircle);
控制聯(lián)合類(lèi)型的分布方式
類(lèi)型推斷是Typescript的本能;大多數(shù)時(shí)候,它公默默地工作。但是,在模糊不清的情況下,我們可能需要干預(yù)。分配條件類(lèi)型就是其中之一。
假設(shè)我們有一個(gè)ToArray輔助類(lèi)型,如果輸入的類(lèi)型不是數(shù)組,則返回一個(gè)數(shù)組類(lèi)型。
type ToArray<T> = T extends Array<unknown> ? T: T[];
你認(rèn)為對(duì)于以下類(lèi)型,應(yīng)該如何推斷?
type Foo = ToArray<string|number>;
答案是string[] | number[]。但這是有歧義的。為什么不是(string | number)[] 呢?
默認(rèn)情況下,當(dāng)typescript遇到一個(gè)聯(lián)合類(lèi)型(這里是string | number)的通用參數(shù)(這里是T)時(shí),它會(huì)分配到每個(gè)組成元素,這就是為什么這里會(huì)得到string[] | number[]。這種行為可以通過(guò)使用特殊的語(yǔ)法和用一對(duì)[]來(lái)包裝T來(lái)改變,比如。
type ToArray<T> = [T] extends [Array<unknown>] ? T : T[]; type Foo = ToArray<string | number>;
現(xiàn)在,Foo 被推斷為類(lèi)型(string | number)[]
使用窮舉式檢查,在編譯時(shí)捕捉未處理的情況
在對(duì)枚舉進(jìn)行 switch-case 操作時(shí),最好是積極地對(duì)不期望的情況進(jìn)行錯(cuò)誤處理,而不是像在其他編程語(yǔ)言中那樣默默地忽略它們:
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
default:
throw new Error('Unknown shape kind');
}
}
使用Typescript,你可以通過(guò)利用never類(lèi)型,讓靜態(tài)類(lèi)型檢查提前為你找到錯(cuò)誤:
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
default:
// 如果任何shape.kind沒(méi)有在上面處理
// 你會(huì)得到一個(gè)類(lèi)型檢查錯(cuò)誤。
const _exhaustiveCheck: never = shape;
throw new Error('Unknown shape kind');
}
}
有了這個(gè),在添加一個(gè)新的shape kind時(shí),就不可能忘記更新getArea函數(shù)。
這種技術(shù)背后的理由是,never 類(lèi)型除了 never 之外不能賦值給任何東西。如果所有的 shape.kind 候選者都被 case 語(yǔ)句消耗完,到達(dá) default 的唯一可能的類(lèi)型就是 never;但是,如果有任何候選者沒(méi)有被覆蓋,它就會(huì)泄漏到 default 分支,導(dǎo)致無(wú)效賦值。
優(yōu)先選擇 type 而不是 interface
在 TypeScript 中,當(dāng)用于對(duì)對(duì)象進(jìn)行類(lèi)型定義時(shí),type 和 interface 構(gòu)造很相似。盡管可能有爭(zhēng)議,但我的建議是在大多數(shù)情況下一貫使用 type,并且僅在下列情況之一為真時(shí)使用 interface:
- 你想利用
interface的 "合并"功能。 - 你有遵循面向?qū)ο箫L(fēng)格的代碼,其中包含類(lèi)/接口層次結(jié)構(gòu)
否則,總是使用更通用的類(lèi)型結(jié)構(gòu)會(huì)使代碼更加一致。
在適當(dāng)?shù)臅r(shí)候優(yōu)先選擇元組而不是數(shù)組
對(duì)象類(lèi)型是輸入結(jié)構(gòu)化數(shù)據(jù)的常見(jiàn)方式,但有時(shí)你可能希望有更多的表示方法,并使用簡(jiǎn)單的數(shù)組來(lái)代替。例如,我們的Circle可以這樣定義:
type Circle = (string | number)[]; const circle: Circle = ['circle', 1.0]; // [kind, radius]
但是這種類(lèi)型檢查太寬松了,我們很容易通過(guò)創(chuàng)建類(lèi)似 ['circle', '1.0'] 的東西而犯錯(cuò)。我們可以通過(guò)使用 Tuple 來(lái)使它更嚴(yán)格:
type Circle = [string, number]; // 這里會(huì)得到一個(gè)錯(cuò)誤 const circle: Circle = ['circle', '1.0'];
Tuple使用的一個(gè)好例子是React的useState:
const [name, setName] = useState('');
它既緊湊又有類(lèi)型安全。
控制推斷的類(lèi)型的通用性或特殊性
在進(jìn)行類(lèi)型推理時(shí),Typescript使用了合理的默認(rèn)行為,其目的是使普通情況下的代碼編寫(xiě)變得簡(jiǎn)單(所以類(lèi)型不需要明確注釋?zhuān)?。有幾種方法可以調(diào)整它的行為。
使用const來(lái)縮小到最具體的類(lèi)型
let foo = { name: 'foo' }; // typed: { name: string }
let Bar = { name: 'bar' } as const; // typed: { name: 'bar' }
let a = [1, 2]; // typed: number[]
let b = [1, 2] as const; // typed: [1, 2]
// typed { kind: 'circle; radius: number }
let circle = { kind: 'circle' as const, radius: 1.0 };
// 如果circle沒(méi)有使用const關(guān)鍵字進(jìn)行初始化,則以下內(nèi)容將無(wú)法正常工作
let shape: { kind: 'circle' | 'rect' } = circle;
使用satisfies來(lái)檢查類(lèi)型,而不影響推斷的類(lèi)型
考慮以下例子:
type NamedCircle = {
radius: number;
name?: string;
};
const circle: NamedCircle = { radius: 1.0, name: 'yeah' };
// error because circle.name can be undefined
console.log(circle.name.length);
我們遇到了錯(cuò)誤,因?yàn)楦鶕?jù)circle的聲明類(lèi)型NamedCircle,name字段確實(shí)可能是undefined,即使變量初始值提供了字符串值。當(dāng)然,我們可以刪除:NamedCircle類(lèi)型注釋?zhuān)覀儗?code>circle對(duì)象的有效性丟失類(lèi)型檢查。相當(dāng)?shù)睦Ь场?/p>
幸運(yùn)的是,Typescript 4.9 引入了一個(gè)新的satisfies關(guān)鍵字,允許你在不改變推斷類(lèi)型的情況下檢查類(lèi)型。
type NamedCircle = {
radius: number;
name?: string;
};
// error because radius violates NamedCircle
const wrongCircle = { radius: '1.0', name: 'ha' }
satisfies NamedCircle;
const circle = { radius: 1.0, name: 'yeah' }
satisfies NamedCircle;
// circle.name can't be undefined now
console.log(circle.name.length);
修改后的版本享有這兩個(gè)好處:保證對(duì)象字面意義符合NamedCircle類(lèi)型,并且推斷出的類(lèi)型有一個(gè)不可為空的名字字段。
使用infer創(chuàng)建額外的泛型類(lèi)型參數(shù)
在設(shè)計(jì)實(shí)用功能和類(lèi)型時(shí),我們經(jīng)常會(huì)感到需要使用從給定類(lèi)型參數(shù)中提取出的類(lèi)型。在這種情況下,infer關(guān)鍵字非常方便。它可以幫助我們實(shí)時(shí)推斷新的類(lèi)型參數(shù)。這里有兩個(gè)簡(jiǎn)單的示例:
// 從一個(gè)Promise中獲取未被包裹的類(lèi)型 // idempotent if T is not Promise type ResolvedPromise<T> = T extends Promise<infer U> ? U : T; type t = ResolvedPromise<Promise<string>>; // t: string // gets the flattened type of array T; // idempotent if T is not array type Flatten<T> = T extends Array<infer E> ? Flatten<E> : T; type e = Flatten<number[][]>; // e: number
T extends Promise<infer U>中的infer關(guān)鍵字的工作方式可以理解為:假設(shè)T與某些實(shí)例化的通用Promise類(lèi)型兼容,即時(shí)創(chuàng)建類(lèi)型參數(shù)U使其工作。因此,如果T被實(shí)例化為Promise<string>,則U的解決方案將是string。
通過(guò)在類(lèi)型操作方面保持創(chuàng)造力來(lái)保持DRY(不重復(fù))
Typescript提供了強(qiáng)大的類(lèi)型操作語(yǔ)法和一套非常有用的工具,幫助你把代碼重復(fù)率降到最低。
不是重復(fù)聲明:
type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = { age: number: gender: string; };
type Geo = { country: string; city: string; };
而是使用Pick工具來(lái)提取新的類(lèi)型:
type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = Pick<User, 'age'|'gender'>;
type Geo = Pick<User, 'country'|'city'>;
不是重復(fù)函數(shù)的返回類(lèi)型
function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}
function transformCircle(circle: { kind: 'circle'; radius: number }) {
...
}
transformCircle(createCircle());
而是使用ReturnType<T>來(lái)提取它:
function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}
function transformCircle(circle: ReturnType<typeof createCircle>) {
...
}
transformCircle(createCircle());
不是并行地同步兩種類(lèi)型的形狀(這里是typeof config和Factory)。
type ContentTypes = 'news' | 'blog' | 'video';
// config for indicating what content types are enabled
const config = { news: true, blog: true, video: false }
satisfies Record<ContentTypes, boolean>;
// factory for creating contents
type Factory = {
createNews: () => Content;
createBlog: () => Content;
};
而是使用Mapped Type和Template Literal Type,根據(jù)配置的形狀自動(dòng)推斷適當(dāng)?shù)墓S類(lèi)型。
type ContentTypes = 'news' | 'blog' | 'video';
// generic factory type with a inferred list of methods
// based on the shape of the given Config
type ContentFactory<Config extends Record<ContentTypes, boolean>> = {
[k in string & keyof Config as Config[k] extends true
? `create${Capitalize<k>}`
: never]: () => Content;
};
// config for indicating what content types are enabled
const config = { news: true, blog: true, video: false }
satisfies Record<ContentTypes, boolean>;
type Factory = ContentFactory<typeof config>;
// Factory: {
// createNews: () => Content;
// createBlog: () => Content;
// }
總結(jié)
本文涵蓋了Typescript語(yǔ)言中的一組相對(duì)高級(jí)的主題。在實(shí)踐中,您可能會(huì)發(fā)現(xiàn)直接使用它們并不常見(jiàn);然而,這些技術(shù)被專(zhuān)門(mén)為T(mén)ypescript設(shè)計(jì)的庫(kù)大量使用:比如Prisma和tRPC。了解這些技巧可以幫助您更好地了解這些工具如何在引擎蓋下工作。
以上就是讓你更好使用Typescript的11個(gè)技巧分享的詳細(xì)內(nèi)容,更多關(guān)于Typescript技巧的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS實(shí)現(xiàn)金額轉(zhuǎn)換(將輸入的阿拉伯?dāng)?shù)字)轉(zhuǎn)換成中文的實(shí)現(xiàn)代碼
這篇文章介紹了JS實(shí)現(xiàn)金額轉(zhuǎn)換(將輸入的阿拉伯?dāng)?shù)字)轉(zhuǎn)換成中文的實(shí)現(xiàn)代碼,有需要的朋友可以參考一下,希望對(duì)大家有用2013-09-09
JS網(wǎng)頁(yè)在線獲取鼠標(biāo)坐標(biāo)值的方法
這篇文章主要介紹了JS網(wǎng)頁(yè)在線獲取鼠標(biāo)坐標(biāo)值的方法,涉及javascript操作頁(yè)面窗口位置元素的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02
JavaScript實(shí)現(xiàn)預(yù)覽本地上傳圖片功能完整示例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)預(yù)覽本地上傳圖片功能,結(jié)合完整實(shí)例形式分析了javascript圖片預(yù)覽相關(guān)的格式正則驗(yàn)證、瀏覽器判斷、頁(yè)面元素屬性動(dòng)態(tài)操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-03-03
微信小程序ReferenceError:xxx?is?not?defined報(bào)錯(cuò)解決辦法
最近在學(xué)習(xí)微信小程序的開(kāi)發(fā),在一個(gè)練手項(xiàng)目中竟然報(bào)錯(cuò),所以下面這篇文章主要給大家介紹了關(guān)于微信小程序ReferenceError:xxx?is?not?defined報(bào)錯(cuò)的解決辦法,需要的朋友可以參考下2023-12-12

