當(dāng)你學(xué)習(xí)TypeScript時(shí),你的第一印象可能會(huì)欺騙你:這不就是JAVAScript注解的一種方式嗎?不就是編譯器用來(lái)幫助我找到潛在bug的嗎?
雖然這種說(shuō)法沒(méi)錯(cuò),但隨著你對(duì)TypeScript不斷了解,你會(huì)發(fā)現(xiàn)這門編程語(yǔ)言最不可思議的力量在于編寫、推斷和操作數(shù)據(jù)類型。
本文總結(jié)的一些技巧,可以幫助大家充分發(fā)揮TypeScript的潛力。
#1 用集合的概念思考問(wèn)題
數(shù)據(jù)類型是程序員日常要處理的概念,但要簡(jiǎn)潔地定義它卻出奇地困難。然而我發(fā)現(xiàn)集合非常適合用作概念模型。
剛開(kāi)始學(xué)習(xí)TypeScript時(shí),我們常常會(huì)發(fā)現(xiàn)用TypeScript編寫類型的方式很不自然。舉一個(gè)非常簡(jiǎn)單的例子:
type Measure = { radius: number };
type Style = { color: string };
// typed { radius: number; color: string }
type Circle = Measure & Style;
如果你在邏輯AND的意義上解釋運(yùn)算符&,可能會(huì)認(rèn)為Circle是一個(gè)虛擬類型,因?yàn)樗莾煞N類型的結(jié)合,沒(méi)有任何重疊的字段。這不是TypeScript的工作方式。此時(shí)通過(guò)集合的概念思考更容易推斷出正確的行為:
- 每個(gè)類型都是一系列值的集合。
- 有些集合是無(wú)限的:例如string、object;有些是有限的:例如bool,undefined,...
- unknown?是通用集(包括所有值),而never是空集(包括無(wú)值)。
- 類型Measure是包含radius數(shù)字字段的所有對(duì)象的集合。style也是如此。
- &?運(yùn)算符創(chuàng)建一個(gè)交集:Measure & Style表示包含radius和color的對(duì)象集,這實(shí)際上是一個(gè)較小的集合,字段更常用。
- 同理,|運(yùn)算符創(chuàng)建一個(gè)并集:一個(gè)較大的集合,但常用字段可能較少(如果組合兩個(gè)對(duì)象類型的話)。
集合還有助于了解可分配性:僅當(dāng)值的類型是目標(biāo)類型的子集時(shí),才允許賦值:
type ShapeKind = 'rect' | 'circle';
let foo: string = getSomeString();
let shape: ShapeKind = 'rect';
// disallowed because string is not subset of ShapeKind
shape = foo;
// allowed because ShapeKind is subset of string
foo = shape;
#2 了解聲明類型和收窄類型
TypeScript中一個(gè)非常強(qiáng)大的功能是基于控制流的自動(dòng)類型收窄。這意味著變量在代碼位置的任何特定點(diǎn)都有兩種與之關(guān)聯(lián)的類型:聲明類型和收窄類型。
function foo(x: string | number) {
if (typeof x === 'string') {
// x's type is narrowed to string, so .length is valid
console.log(x.length);
// assignment respects declaration type, not narrowed type
x = 1;
console.log(x.length); // disallowed because x is now number
} else {
...
}
}
#3 使用可區(qū)分的聯(lián)合類型而不是可選字段
當(dāng)定義一組多態(tà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)閗ind和其他字段之間沒(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;
}
從以上代碼可以看出,類型收窄消除了強(qiáng)制類型轉(zhuǎn)換的需要。
#4 使用類型謂詞避免類型斷言
如果你以正確的方式使用TypeScript的話,你會(huì)發(fā)現(xiàn)自己很少使用顯式類型斷言(比如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();
// error because typescript doesn't know the filtering
// narrows typing
const circles: Circle[] = myShapes.filter(isCircle);
// you may be inclined to add an assertion:
// const circles = myShapes.filter(isCircle) as Circle[];
更優(yōu)雅的解決方案是將isCircle和isRect?更改為返回類型謂詞,這樣就可以幫助TypeScript在filter調(diào)用后進(jìn)一步收窄類型:
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);
#5 控制聯(lián)合類型的分布方式
類型推斷是TypeScript的特性;大多數(shù)時(shí)候,它默默地為你工作。但是有時(shí)你可能對(duì)模棱兩可的細(xì)微情況進(jìn)行干預(yù)。分布式條件類型就是其中一種情況。
假設(shè)我們有一個(gè)ToArray輔助類,如果輸入類型還不是數(shù)組類型,則返回?cái)?shù)組類型:
type ToArray<T> = T extends Array<unknown> ? T: T[];
你認(rèn)為以下類型會(huì)推斷出什么?
type Foo = ToArray<string|number>;
答案是string[] | number[]?。但這是模棱兩可的。為什么不是(string | number)[]呢?
默認(rèn)情況下,當(dāng)TypeScript遇到聯(lián)合類型(此處為string | number?)的泛型參數(shù)(此處為T?)時(shí),它會(huì)分布到每個(gè)組成部分中,這就是為什么會(huì)得到string[] | number[]?的原因。你可以通過(guò)使用特殊語(yǔ)法并將T?包裝在一對(duì)[]中來(lái)更改此行為,例如:
type ToArray<T> = [T] extends [Array<unknown>] ? T : T[];
type Foo = ToArray<string | number>;
現(xiàn)在Foo?被推斷為類型(string | number)[]。
#6 使用詳盡檢查捕獲在編譯時(shí)未處理的情況
在switch?語(yǔ)句中使用enum枚舉時(shí),一個(gè)好習(xí)慣是在沒(méi)有匹配到合適值的情況下主動(dòng)拋錯(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');
}
}
通過(guò)使用never類型,靜態(tà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:
// you'll get a type-checking error below
// if any shape.kind is not handled above
const _exhaustiveCheck: never = shape;
throw new Error('Unknown shape kind');
}
}
有了這個(gè),在添加新的shape?種類時(shí),就不可能忘記更新getArea函數(shù)。
該技術(shù)背后的基本原理是,除了never?之外,不能為never?類型分配任何內(nèi)容。如果shape.kind?的所有備選項(xiàng)都被case?語(yǔ)句用盡,那么達(dá)到default?的唯一可能類型是never?;但是,如果未涵蓋所有備選項(xiàng),則將泄漏到default分支并導(dǎo)致無(wú)效分配。
#7 寧可使用type而不是interface
在TypeScript中,type和interface?是兩種非常相似的數(shù)據(jù)結(jié)構(gòu),都可以用來(lái)構(gòu)造復(fù)雜的對(duì)象的。雖然可能有爭(zhēng)議,但我的建議是在大多數(shù)情況下始終使用type,僅在滿足以下任一條件時(shí)才使用interface:
- 想利用interface的合并功能。
- 有涉及類/接口層次結(jié)構(gòu)的OO樣式代碼。
否則,始終使用更通用的type構(gòu)造會(huì)產(chǎn)生更一致的代碼。
#8 只要合適寧可使用元組而不是數(shù)組
對(duì)象類型是構(gòu)造結(jié)構(gòu)化數(shù)據(jù)的常用方法,但有時(shí)你可能希望使用更簡(jiǎn)潔的表示形式,而改用簡(jiǎn)單的數(shù)組。例如,Circle可以定義為:
type Circle = (string | number)[];
const circle: Circle = ['circle', 1.0]; // [kind, radius]
但是這種構(gòu)造是松散的,如果創(chuàng)建類似['circle', '1.0']的內(nèi)容很容易出錯(cuò)。我們可以通過(guò)使用元組來(lái)使其更嚴(yán)格:
type Circle = [string, number];
// you'll get an error below
const circle: Circle = ['circle', '1.0'];
使用元組的一個(gè)很好的例子是React中的useState。
const [name, setName] = useState('');
既緊湊又類型安全。
#9 控制推斷類型的通用性或特殊性
TypeScript在進(jìn)行類型推斷時(shí)使用合理的默認(rèn)行為,旨在使常見(jiàn)情況下的代碼編寫變得容易(因此類型不需要顯式注釋)。有幾種方法可以調(diào)整其行為。
- 使用const縮小到最具體的類型
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 };
// the following won't work if circle wasn't initialized
// with the const keyword
let shape: { kind: 'circle' | 'rect' } = circle;
- 使用satisfies來(lái)檢查類型,而不影響推斷的類型
請(qǐng)看以下示例:
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);
有個(gè)錯(cuò)誤,這是因?yàn)楦鶕?jù)circle?的聲明類型NamedCircle,name?字段確實(shí)可以未定義,即使變量初始值設(shè)定項(xiàng)提供了字符串值。當(dāng)然,我們可以刪除:NamedCircle?類型注釋,但這將松散對(duì)circle對(duì)象有效性的類型檢查。進(jìn)退兩難。
幸運(yùn)的是,Typescript 4.9引入了一個(gè)新的satisfies關(guān)鍵字,它允許你在不更改推斷類型的情況下檢查類型:
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è)優(yōu)點(diǎn):對(duì)象字面量保證符合NamedCircle類型,推斷類型具有不可為空的名稱字段。
#10 使用infer創(chuàng)建額外的泛型類型參數(shù)
在設(shè)計(jì)實(shí)用工具函數(shù)和類型時(shí),你經(jīng)常會(huì)覺(jué)得需要使用從給定類型參數(shù)中提取的類型。在這種情況下,infer關(guān)鍵字就可以派上用場(chǎng)。它可以幫助快速推斷新的類型參數(shù)。下面是兩個(gè)簡(jiǎn)單的例子:
// gets the unwrApped type out of a Promise;
// 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
infer?關(guān)鍵字在T extends Promise<infer U>?中的工作原理可以理解為:假設(shè)T?與一些實(shí)例化的泛型Promise類型兼容,臨時(shí)湊合一個(gè)類型參數(shù)U?以使其工作。因此,如果T?被實(shí)例化為Promise<string>?,則U?的解決方案將是string。
#11 創(chuàng)新類型操作以保持DRY
TypeScript提供了強(qiáng)大的類型操作語(yǔ)法和一組非常有用的實(shí)用程序,可幫助你將代碼重復(fù)減少到最低限度。以下是一些簡(jiǎn)單示例:
與其重復(fù)字段聲明:
type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = { age: number: gender: string; };
type Geo = { country: string; city: string; };
還不如使用pick實(shí)用程序提取新類型:
type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = Pick<User, 'age'|'gender'>;
type Geo = Pick<User, 'country'|'city'>;
與其復(fù)制函數(shù)的返回類型:
function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}
function transformCircle(circle: { kind: 'circle'; radius: number }) {
...
}
transformCircle(createCircle());
還不如使用ReturnType<T>提?。?/p>
function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}
function transformCircle(circle: ReturnType<typeof createCircle>) {
...
}
transformCircle(createCircle());
與其并行同步兩種類型的shape?(此處為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;
};
還不如使用映射類型和模板字面量類型根據(jù)config?的形狀自動(dòng)推斷正確的factory類型:
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ǔ)言的高級(jí)應(yīng)用。在實(shí)踐中,你可能會(huì)發(fā)現(xiàn)直接這樣用并不常見(jiàn);但是,這些技術(shù)被大量用于那些專門為TypeScript而設(shè)計(jì)的庫(kù):如Prisma和tRPC。了解這些技巧可以幫助你更好地理解這些工具是發(fā)揮其威力的。