最新要聞

廣告

5G

首例5g乳腺手術價格是多少錢?首例5g乳腺手術成功率是多少?

首例5g乳腺手術價格是多少錢?首例5g乳腺手術成功率是多少?

電信光纖多少錢一年?電信光纖價格表

電信光纖多少錢一年?電信光纖價格表

科技

TypeScript 程序員晉級的 11 個必備技巧

來源:前端新世界

當你學習TypeScript時,你的第一印象可能會欺騙你:這不就是JavaScript注解的一種方式嗎?不就是編譯器用來幫助我找到潛在bug的嗎?


(資料圖)

雖然這種說法沒錯,但隨著你對TypeScript不斷了解,你會發現這門編程語言最不可思議的力量在于編寫、推斷和操作數據類型。

本文總結的一些技巧,可以幫助大家充分發揮TypeScript的潛力。

#1 用集合的概念思考問題

數據類型是程序員日常要處理的概念,但要簡潔地定義它卻出奇地困難。然而我發現集合非常適合用作概念模型。

剛開始學習TypeScript時,我們常常會發現用TypeScript編寫類型的方式很不自然。舉一個非常簡單的例子:

type Measure = { radius: number };type Style = { color: string };// typed { radius: number; color: string }type Circle = Measure & Style;

如果你在邏輯AND的意義上解釋運算符&,可能會認為Circle是一個虛擬類型,因為它是兩種類型的結合,沒有任何重疊的字段。這不是TypeScript的工作方式。此時通過集合的概念思考更容易推斷出正確的行為:

每個類型都是一系列值的集合。有些集合是無限的:例如string、object;有些是有限的:例如bool,undefined,...unknown?是通用集(包括所有值),而never是空集(包括無值)。類型Measure是包含radius數字字段的所有對象的集合。style也是如此。&?運算符創建一個交集:Measure & Style表示包含radius和color的對象集,這實際上是一個較小的集合,字段更常用。同理,|運算符創建一個并集:一個較大的集合,但常用字段可能較少(如果組合兩個對象類型的話)。

集合還有助于了解可分配性:僅當值的類型是目標類型的子集時,才允許賦值:

type ShapeKind = "rect" | "circle";let foo: string = getSomeString();let shape: ShapeKind = "rect";// disallowed because string is not subset of ShapeKindshape = foo;// allowed because ShapeKind is subset of stringfoo = shape;
#2 了解聲明類型和收窄類型

TypeScript中一個非常強大的功能是基于控制流的自動類型收窄。這意味著變量在代碼位置的任何特定點都有兩種與之關聯的類型:聲明類型和收窄類型。

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 使用可區分的聯合類型而不是可選字段

當定義一組多態類型(如Shape)時,很容易這樣開始寫代碼:

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!;}

需要非空斷言(訪問radius、width和height?時),因為kind和其他字段之間沒有建立關系。相反,可區分的聯合類型是一個更好的解決方案:

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;}

從以上代碼可以看出,類型收窄消除了強制類型轉換的需要。

#4 使用類型謂詞避免類型斷言

如果你以正確的方式使用TypeScript的話,你會發現自己很少使用顯式類型斷言(比如value as SomeType);但是,有時你可能會沖動地寫出諸如這樣的代碼:

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 typingconst circles: Circle[] = myShapes.filter(isCircle);// you may be inclined to add an assertion:// const circles = myShapes.filter(isCircle) as Circle[];

更優雅的解決方案是將isCircle和isRect?更改為返回類型謂詞,這樣就可以幫助TypeScript在filter調用后進一步收窄類型:

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 correctlyconst circles = myShapes.filter(isCircle);
#5 控制聯合類型的分布方式

類型推斷是TypeScript的特性;大多數時候,它默默地為你工作。但是有時你可能對模棱兩可的細微情況進行干預。分布式條件類型就是其中一種情況。

假設我們有一個ToArray輔助類,如果輸入類型還不是數組類型,則返回數組類型:

type ToArray = T extends Array ? T: T[];

你認為以下類型會推斷出什么?

type Foo = ToArray;

答案是string[] | number[]?。但這是模棱兩可的。為什么不是(string | number)[]呢?

默認情況下,當TypeScript遇到聯合類型(此處為string | number?)的泛型參數(此處為T?)時,它會分布到每個組成部分中,這就是為什么會得到string[] | number[]?的原因。你可以通過使用特殊語法并將T?包裝在一對[]中來更改此行為,例如:

type ToArray = [T] extends [Array] ? T : T[];type Foo = ToArray;

現在Foo?被推斷為類型(string | number)[]。

#6 使用詳盡檢查捕獲在編譯時未處理的情況

在switch?語句中使用enum枚舉時,一個好習慣是在沒有匹配到合適值的情況下主動拋錯,而不是像在其他編程語言中那樣默默地忽略它們:

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");  }}

通過使用never類型,靜態類型檢查就可以更早地查找到錯誤:

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");  }}

有了這個,在添加新的shape?種類時,就不可能忘記更新getArea函數。

該技術背后的基本原理是,除了never?之外,不能為never?類型分配任何內容。如果shape.kind?的所有備選項都被case?語句用盡,那么達到default?的唯一可能類型是never?;但是,如果未涵蓋所有備選項,則將泄漏到default分支并導致無效分配。

#7 寧可使用type而不是interface

在TypeScript中,type和interface?是兩種非常相似的數據結構,都可以用來構造復雜的對象的。雖然可能有爭議,但我的建議是在大多數情況下始終使用type,僅在滿足以下任一條件時才使用interface:

想利用interface的合并功能。有涉及類/接口層次結構的OO樣式代碼。

否則,始終使用更通用的type構造會產生更一致的代碼。

#8 只要合適寧可使用元組而不是數組

對象類型是構造結構化數據的常用方法,但有時你可能希望使用更簡潔的表示形式,而改用簡單的數組。例如,Circle可以定義為:

type Circle = (string | number)[];const circle: Circle = ["circle", 1.0];  // [kind, radius]

但是這種構造是松散的,如果創建類似["circle", "1.0"]的內容很容易出錯。我們可以通過使用元組來使其更嚴格:

type Circle = [string, number];// you"ll get an error belowconst circle: Circle = ["circle", "1.0"];

使用元組的一個很好的例子是React中的useState。

const [name, setName] = useState("");

既緊湊又類型安全。

#9 控制推斷類型的通用性或特殊性

TypeScript在進行類型推斷時使用合理的默認行為,旨在使常見情況下的代碼編寫變得容易(因此類型不需要顯式注釋)。有幾種方法可以調整其行為。

使用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 keywordlet shape: { kind: "circle" | "rect" } = circle;
使用satisfies來檢查類型,而不影響推斷的類型

請看以下示例:

type NamedCircle = {    radius: number;    name?: string;};const circle: NamedCircle = { radius: 1.0, name: "yeah" };// error because circle.name can be undefinedconsole.log(circle.name.length);

有個錯誤,這是因為根據circle?的聲明類型NamedCircle,name?字段確實可以未定義,即使變量初始值設定項提供了字符串值。當然,我們可以刪除:NamedCircle?類型注釋,但這將松散對circle對象有效性的類型檢查。進退兩難。

幸運的是,Typescript 4.9引入了一個新的satisfies關鍵字,它允許你在不更改推斷類型的情況下檢查類型:

type NamedCircle = {    radius: number;    name?: string;};// error because radius violates NamedCircleconst wrongCircle = { radius: "1.0", name: "ha" }    satisfies NamedCircle;const circle = { radius: 1.0, name: "yeah" }    satisfies NamedCircle;// circle.name can"t be undefined nowconsole.log(circle.name.length);

修改后的版本具有兩個優點:對象字面量保證符合NamedCircle類型,推斷類型具有不可為空的名稱字段。

#10 使用infer創建額外的泛型類型參數

在設計實用工具函數和類型時,你經常會覺得需要使用從給定類型參數中提取的類型。在這種情況下,infer關鍵字就可以派上用場。它可以幫助快速推斷新的類型參數。下面是兩個簡單的例子:

// gets the unwrapped type out of a Promise;// idempotent if T is not Promisetype ResolvedPromise = T extends Promise ? U : T;type t = ResolvedPromise>; // t: string// gets the flattened type of array T;// idempotent if T is not arraytype Flatten = T extends Array ? Flatten : T;type e = Flatten; // e: number

infer?關鍵字在T extends Promise?中的工作原理可以理解為:假設T?與一些實例化的泛型Promise類型兼容,臨時湊合一個類型參數U?以使其工作。因此,如果T?被實例化為Promise?,則U?的解決方案將是string。

#11 創新類型操作以保持DRY

TypeScript提供了強大的類型操作語法和一組非常有用的實用程序,可幫助你將代碼重復減少到最低限度。以下是一些簡單示例:

與其重復字段聲明:

type User = {    age: number;    gender: string;    country: string;    city: string};type Demographic = { age: number: gender: string; };type Geo = { country: string; city: string; };

還不如使用pick實用程序提取新類型:

type User = {    age: number;    gender: string;    country: string;    city: string};type Demographic = Pick;type Geo = Pick;

與其復制函數的返回類型:

function createCircle() {    return {        kind: "circle" as const,        radius: 1.0    }}function transformCircle(circle: { kind: "circle"; radius: number }) {    ...}transformCircle(createCircle());

還不如使用ReturnType提取:

function createCircle() {    return {        kind: "circle" as const,        radius: 1.0    }}function transformCircle(circle: ReturnType) {    ...}transformCircle(createCircle());

與其并行同步兩種類型的shape?(此處為config?類型和Factory):

type ContentTypes = "news" | "blog" | "video";// config for indicating what content types are enabledconst config = { news: true, blog: true, video: false }    satisfies Record;// factory for creating contentstype Factory = {    createNews: () => Content;    createBlog: () => Content;};

還不如使用映射類型和模板字面量類型根據config?的形狀自動推斷正確的factory類型:

type ContentTypes = "news" | "blog" | "video";// generic factory type with a inferred list of methods// based on the shape of the given Configtype ContentFactory> = {    [k in string & keyof Config as Config[k] extends true        ? `create${Capitalize}`        : never]: () => Content;};// config for indicating what content types are enabledconst config = { news: true, blog: true, video: false }    satisfies Record;type Factory = ContentFactory;// Factory: {//     createNews: () => Content;//     createBlog: () => Content; // }
總結

這篇文章介紹了一系列TypeScript語言的高級應用。在實踐中,你可能會發現直接這樣用并不常見;但是,這些技術被大量用于那些專門為TypeScript而設計的庫:如Prisma和tRPC。了解這些技巧可以幫助你更好地理解這些工具是發揮其威力的。

關鍵詞: