
簡介這是一份面向前端開發(fā)者及TypeScript初學者的PDF教程以數(shù)據(jù)類型為主線系統(tǒng)梳理類型系統(tǒng)的核心概念適合用于快速入門、查漏補缺或作為課程配套資料。資源僅包含1個PDF文檔壓縮包大小約1.75MB小而精便于下載后隨時隨地學習。目前該資料已有1099人學習內(nèi)容覆蓋基礎(chǔ)數(shù)據(jù)類型、枚舉、接口、類等知識點。教程從布爾值、數(shù)字、字符串、數(shù)組、元組等基礎(chǔ)類型講起再深入到外部枚舉、接口的可選屬性、函數(shù)類型、數(shù)組類型、類類型與實現(xiàn)接口并配有大量可直接運行的代碼示例。此外還涉及類靜態(tài)部分與實例部分的區(qū)別、擴展接口、混合類型、接口繼承類等進階主題章節(jié)安排循序漸進目錄與導航清晰方便讀者按需查閱。完整學完后讀者能夠理解類型注解、接口約束與類結(jié)構(gòu)的配合方式從而在實際項目中寫出更健壯、可維護的TypeScript代碼。1. TypeScript 教程資源從類型語法到工程習慣的進階路線TypeScript 教程網(wǎng)上多到看不完但多數(shù)人卡住的位置不是語法本身而是接口、泛型、模塊組織這些看著能懂、一寫就錯的部分。這份教程資源的目錄編排順序很對我的胃口先基礎(chǔ)數(shù)據(jù)類型再接口、類和函數(shù)接著泛型與類型推導最后落到命名空間與模塊末尾還專門寫了類型兼容性和 .d.ts 定義文件的編寫。它不是一張速查表而是一條從會寫注解到能設(shè)計類型的完整路線。適合已經(jīng)寫過一些 TS 但總在編譯報錯里打轉(zhuǎn)的前端開發(fā)者也適合想系統(tǒng)梳理類型知識的中級工程師照著過一遍把零散經(jīng)驗串成體系。2. 把類型基礎(chǔ)打牢基礎(chǔ)數(shù)據(jù)類型、接口與類的實戰(zhàn)邊界2.1 基礎(chǔ)數(shù)據(jù)類型boolean、number、string、array 與 tuple 的邊界教程開篇用很小的篇幅把基礎(chǔ)類型過了一遍這幾行代碼看起來簡單但里面藏著不少邊界。let isDone: boolean false; let decLiteral: number 6; let hexLiteral: number 0xf00d; let binaryLiteral: number 0b1010; let octalLiteral: number 0o744; let myName: string Tom; let myAge: string 25;所有數(shù)值統(tǒng)一用number表示不區(qū)分整型和浮點型這是 TypeScript 和 Java 系語言最明顯的差異也是很多后端轉(zhuǎn)前端的同事第一周最容易不習慣的地方。寫進制字面量時要注意0b和0o這類寫法在舊版 ES target 下需要編譯器轉(zhuǎn)譯如果項目里 tsconfig 的 target 設(shè)成 ES3 或很老的版本編譯產(chǎn)物會變長極端情況下還會出現(xiàn)兼容問題。字符串的單雙引號完全等價重點不在于用哪種而在于團隊 lint 規(guī)則統(tǒng)一成一種。數(shù)組有兩種寫法語義是等價的。let list1: number[] [1, 2, 3]; let list2: Arraynumber [1, 2, 3];泛型形式Arraynumber在函數(shù)簽名里更常用因為它可以方便地組合出更復(fù)雜的類型比如Arraystring | number。而number[]寫法更直觀日常聲明局部變量時我一般用方括號形式。兩種寫法在類型層面沒有任何區(qū)別不需要糾結(jié)但同一個文件里最好保持一致。元組是初學者最早遇到的坑之一。let x: [string, number]; x [hello, 10]; // OK x [10, hello]; // Error元組聲明了固定長度和固定位置類型但實際運行時數(shù)組的長度約束比想象中弱。在后面避坑章節(jié)我會專門展開這里先記住一個結(jié)論元組只保證前幾個位置的類型不保證越界訪問的行為業(yè)務(wù)代碼里能用對象就別用長元組。枚舉這塊教程里同時講了普通枚舉和外部枚舉。enum Color {Red, Green, Blue} let c: Color Color.Green;數(shù)字枚舉編譯后會生成反向映射Color[0]能拿到Red字符串枚舉則沒有這個反向映射這是兩者行為上最核心的差異。外部枚舉用declare enum聲明只描述類型不生成實現(xiàn)代碼適合對接已有的 JavaScript 全局變量。寫外部枚舉時要注意它和運行時真實對象一旦不一致類型層面完全看不出來屬于典型的編譯過了但跑起來才知道的坑。2.2 接口對象契約、函數(shù)形狀與類實現(xiàn)接口是這份教程里篇幅最長、例子最多的部分因為它是 TypeScript 類型系統(tǒng)的骨架。interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); }接口是結(jié)構(gòu)化類型只要形狀匹配就能通過不要求對象顯式implements。這是 TypeScript 和 Java 最大的思維差異。一個對象哪怕聲明時根本沒提接口只要它有l(wèi)abel: string就能傳給這個函數(shù)。這個設(shè)計讓 TypeScript 能輕松對接無類型的歷史代碼但也意味著你要接受一種沒寫契約但契約存在的狀態(tài)??蛇x屬性用?標記這是接口最常見的形態(tài)之一。interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare {color: white, area: 100}; if (config.color) { newSquare.color config.color; } if (config.width) { newSquare.area config.width * config.width; } return newSquare; }可選屬性配合--strictNullChecks開啟后config.color的類型是string | undefined你必須先判斷再使用。教程里這段代碼寫得比較保守現(xiàn)實中我一般會直接用config.color ?? white這種默認值寫法代碼更短也不用擔心漏掉判斷分支。函數(shù)類型接口專門用來描述函數(shù)的形狀它把函數(shù)當成對象來做約束。interface SearchFunc { (source: string, subString: string): boolean; }這種接口特別適合回調(diào)、事件處理器和策略模式。和直接寫(source: string, subString: string) boolean相比接口形式的優(yōu)勢是可以被擴展、被合并在復(fù)雜類型體系里更好維護。類類型接口和擴展接口是另一個重點。interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date new Date(); }這里有個經(jīng)典限制接口只能約束類的實例部分不能約束構(gòu)造函數(shù)本身。教程里專門講了類靜態(tài)部分與實例部分的區(qū)別意圖就在這里。構(gòu)造函數(shù)是靜態(tài)側(cè)的成員implements管不到它。如果你想讓某個工廠函數(shù)接受能 new 出特定形狀的類得把new簽名寫在一個獨立接口上而不是期望類去implements它。這個點很容易繞暈記住一句話implements管實例new簽名管構(gòu)造。2.3 類的訪問控制public、private、protected 與參數(shù)屬性類這一章教程從基礎(chǔ)的訪問修飾符講起一直延伸到抽象類。class Animal { public name: string; private age: number; protected sex: string; constructor(name: string, age: number) { this.name name; this.age age; } }public是默認值private只允許本類內(nèi)部訪問protected允許子類訪問。這套訪問控制在編譯期是嚴格的但要注意它只存在于類型系統(tǒng)里編譯成 JavaScript 后就什么保護都沒有了運行時任何人都能訪問。如果你需要運行時層面的真正私有得用 ES2022 的#私有字段。參數(shù)屬性是 TypeScript 提供的語法糖把聲明和賦值壓縮進構(gòu)造函數(shù)參數(shù)。class Animal { constructor(private name: string, protected age: number) {} }這種寫法代碼量少但閱讀門檻偏高。新人看到constructor(private name: string)不一定能立刻反應(yīng)過來這里隱式創(chuàng)建了同名字段。我的習慣是業(yè)務(wù)類隨便用公共基類和工具類盡量寫顯式字段減少團隊理解成本。存取器和靜態(tài)屬性也在這章有一個高頻坑值得提前說。class Employee { private _fullName: string ; get fullName(): string { return this._fullName; } set fullName(name: string) { this._fullName name; } static company: string 某公司; }getter/setter 編譯到 ES5 時會生成Object.defineProperty屬性和 getter 不能撞名否則會無限遞歸觸發(fā) getter 導致棧溢出。教程里用_fullName加下劃線就是這個原因不是為了好看。2.4 抽象類與繼承什么時候該抽基類抽象類在教程里排在類這一章的后半段但它其實是設(shè)計層面的內(nèi)容。abstract class Animal { abstract makeSound(): void; move(): void { console.log(moving); } } class Dog extends Animal { makeSound(): void { console.log(bark); } }抽象類不能被實例化抽象方法必須在子類實現(xiàn)普通方法可以繼承默認行為。它和接口的分工很清晰接口定義有什么抽象類定義是什么以及默認怎么做。實際項目里抽象類適合做那些需要共享狀態(tài)和方法的基類比如組件基類、服務(wù)基類如果只是約束形狀接口足夠不需要抽象類的重量。教程在這一章還提了一個進階技巧把類當作接口使用。類定義本身會生成兩個東西一個是實例類型一個是構(gòu)造函數(shù)值。當你把類當接口用時類中的private或protected成員會參與結(jié)構(gòu)匹配兩個類就算形狀一樣只要私有成員來源不同類型就不兼容。這個規(guī)則在后面的類型兼容性章節(jié)還會再遇到但要先在這個地方留下印象。3. 函數(shù)、泛型與類型推導用類型約束邏輯3.1 函數(shù)類型與重載完整函數(shù)類型寫法函數(shù)這一章教程先講如何給函數(shù)定義類型再講完整函數(shù)類型的寫法。let myAdd: (x: number, y: number) number function(x: number, y: number): number { return x y; };左邊是函數(shù)類型變量聲明右邊是具體實現(xiàn)。這里有個細節(jié)實現(xiàn)函數(shù)的參數(shù)名可以跟類型簽名里的參數(shù)名不一致只要位置和類型匹配就行。類型系統(tǒng)檢查的是形狀不是名字。返回值類型寫在箭頭后面如果實現(xiàn)里返回了非 number 類型編譯器會立即報錯。重載是函數(shù)章節(jié)里含金量最高的部分。function pickCard(x: {suit: string; card: number}[]): number; function pickCard(x: number): {suit: string; card: number}; function pickCard(x: any): any { return typeof x number ? {suit: spade, card: x} : x[0]; }重載列表從上到下依次匹配越具體的簽名放越前面最后一個實現(xiàn)簽名不會暴露給調(diào)用方。新手最常見的錯誤是把any版本放在最上面導致所有調(diào)用都走any分支類型保護形同虛設(shè)。這是典型的看著能跑、類型全丟的寫法編輯器不會報錯但寫完了跟沒寫一樣。3.2 可選參數(shù)、默認參數(shù)與剩余參數(shù)function buildName(firstName: string, lastName?: string, age: number 18) { return firstName (lastName || ) age; }可選參數(shù)必須放在必選參數(shù)后面這是 TypeScript 的硬性規(guī)則。默認參數(shù)表面上可以放在任意位置但調(diào)用時會有歧義比如buildName(Tom, undefined, 20)這種傳法可讀性很差。我的建議是默認參數(shù)統(tǒng)一放最后不到萬不得已別放中間。剩余參數(shù)處理不定數(shù)量的入?yún)?。function join(...parts: string[]): string { return parts.join(-); }剩余參數(shù)的類型必須是數(shù)組在 ES5 目標下編譯后會生成arguments的循環(huán)拷貝參數(shù)數(shù)量大時會有可見的性能開銷。這個一般不會成為瓶頸但如果你寫的是一個會被高頻調(diào)用的工具函數(shù)可以考慮用普通參數(shù)數(shù)組替代剩余參數(shù)。3.3 泛型從 Hello World 到約束泛型是這份教程里最值得反復(fù)讀的部分因為它是從約束類型走向設(shè)計類型的分水嶺。function identityT(arg: T): T { return arg; }泛型的核心價值是保留類型關(guān)系輸入什么類型就返回什么類型。兩個常見誤用一是用any代替T返回值變成了any類型關(guān)系徹底丟失二是在泛型函數(shù)里直接訪問.length編譯器會報錯因為泛型不知道參數(shù)具體形狀。正確的做法是用泛型約束。interface Lengthwise { length: number; } function loggingIdentityT extends Lengthwise(arg: T): T { console.log(arg.length); return arg; }T extends Lengthwise的意思是T 必須滿足 Lengthwise 結(jié)構(gòu)這樣函數(shù)體內(nèi)就可以安全訪問.length。教程后面還有一段在泛型約束中使用類型參數(shù)的例子這個是高頻考點。function getPropertyT, K extends keyof T(obj: T, key: K) { return obj[key]; }K extends keyof T約束了第二個參數(shù)必須是 T 的鍵之一調(diào)用時對象和鍵的對應(yīng)關(guān)系由編譯器保證。如果傳入一個對象上不存在的鍵編譯期直接報錯而不是運行時返回 undefined。這個模式在寫表單映射、狀態(tài)管理的時候非常實用。泛型類也要注意一個硬限制。class GenericNumberT { zeroValue: T; add: (x: T, y: T) T; }泛型類只作用于實例側(cè)靜態(tài)成員不能使用類型參數(shù)。這是編譯器的明確限制想寫靜態(tài)泛型方法只能把它挪到類外部的獨立函數(shù)里。3.4 類型推導與類型兼容性類型推導這章講的是不寫類型時 TypeScript 怎么猜。原則很簡單能推導就不需要顯式標注顯式標注超過必要范圍會降低代碼可讀性。let x [0, 1, null]; // 在 strictNullChecks 下推導為 (number | null)[]最佳通用類型算法會從數(shù)組元素里找出所有候選類型然后計算一個兼容所有元素的聯(lián)合類型。上下文類型是反向推導左邊變量給右邊函數(shù)表達式提供了類型環(huán)境這時候?qū)崿F(xiàn)里的參數(shù)可以省略類型標注。const handler: (e: MouseEvent) void e { console.log(e.clientX); };類型兼容性章節(jié)的核心是結(jié)構(gòu)子類型成員多的類型可以賦給成員少的類型。兩個函數(shù)比較時參數(shù)允許雙向協(xié)變不過開啟strictFunctionTypes后函數(shù)類型參數(shù)的檢查會收緊只允許逆變。枚舉之間不兼容就算枚舉值一模一樣也不能互相賦值。類的私有成員必須同源才兼容這就是前面類那一章強調(diào)的規(guī)則在這里落地。4. 命名空間與模塊代碼組織與打包的取舍4.1 命名空間 vs 模塊選型標準教程里命名空間和模塊這一章反復(fù)強調(diào)一個觀點優(yōu)先使用模塊謹慎使用命名空間。這個結(jié)論要放在歷史背景下理解。命名空間是早期 TypeScript 用來組織全局代碼的方式它把變量掛在一個全局對象下靠限定名防止污染。模塊則是標準的文件作用域加顯式導入導出是現(xiàn)代工程里唯一合理的組織方式。選型標準很簡單項目是現(xiàn)代化構(gòu)建流程比如 webpack、vite、ts-node一律用模塊寫的是純?yōu)g覽器腳本沒有構(gòu)建步驟或者給全局變量庫寫聲明才考慮命名空間。教程里那句對模塊使用 reference 是陷阱說的就是把這兩種機制混在一起的情況。4.2 export 與 import 的推薦姿勢模塊章節(jié)把導出導入的語法形式都過了一遍這里整理成一張常用對照表。場景寫法說明導出聲明export interface Thing跟隨聲明直接導出導出語句export { thing }把已定義變量導出重導出export { thing as other } from ./x轉(zhuǎn)發(fā)并改名默認導出export default function() {}單文件單入口導入單個import { thing } from ./x按需導入導入整體import * as m from ./x掛在命名空間對象上副作用導入import ./polyfill只執(zhí)行模塊代碼副作用導入要特別注意順序因為它不取任何綁定純粹執(zhí)行模塊內(nèi)部邏輯。如果模塊里有初始化代碼依賴另一個模塊而你把它排在前面運行時可能拿到未初始化的狀態(tài)。這種問題不會在編譯期暴露只能等運行時崩。4.3 模塊結(jié)構(gòu)指導頂層導出優(yōu)先教程這段寫得像工程規(guī)范每條都是實戰(zhàn)里沉淀出來的。盡可能在頂層導出避免嵌套導出。如果只導出一個 class 或 function用export default。導出多個對象時全部放頂層導出。要導出大量內(nèi)容時用命名空間導入模式接收。擴展第三方模塊時用重新導出去包裝。這些規(guī)則的目的是讓導入方一眼看到 API 表面。嵌套導出會讓調(diào)用方寫出ns1.ns2.ns3.foo()這種路徑編輯器提示都沒法用。我見過一個老項目把命名空間套了三層最后每個人導入的時候都要先翻一翻對象結(jié)構(gòu)可讀性代價非常大。4.4 模塊加載與命名空間混用的坑教程明確列了幾個陷阱值得單獨拎出來。對模塊使用/// reference是反模式reference 指令只適合全局腳本和聲明文件模塊之間應(yīng)該用 import 建立依賴關(guān)系。把 reference 用在模塊文件里編譯器雖然能識別但會把依賴關(guān)系搞亂構(gòu)建工具拿到的是物理路徑而不是模塊解析路徑打包順序容易出問題。教程里不必要的命名空間這一節(jié)也很直接模塊文件里再包一層命名空間等于把已經(jīng)隔離的作用域又套了個殼純粹增加噪音。模塊本身就有作用域再包命名空間沒有任何額外收益只有查詢路徑變長的成本。5. 避坑與排查TS 學習路上最常見的 5 個翻車現(xiàn)場5.1 元組越界訪問類型上已失控運行時靜默現(xiàn)象定義let x: [string, number] [hello, 10]后調(diào)用x.push(true)編譯器不報錯訪問x[3]也拿不到任何類型提示運行時返回 undefined數(shù)據(jù)悄悄丟了一截。原因早期版本對元組的約束只覆蓋聲明過的索引位置越界操作走的是數(shù)組放寬邏輯類型檢查形同虛設(shè)。這個問題在舊版編譯配置下非常隱蔽因為它不報錯。解決開啟--noUncheckedIndexedAccess讓越界訪問的類型變成undefined逼你先做判斷業(yè)務(wù)上優(yōu)先用對象結(jié)構(gòu)替代長元組或者用readonly元組加as const固定數(shù)據(jù)。從那以后我寫元組前都會先問一句這里是不是其實應(yīng)該用對象。5.2 數(shù)字枚舉的反向映射刪掉的值還活著現(xiàn)象enum Color {Red, Green, Blue}里刪掉 Green代碼里沒報錯但運行時Color[1]依然返回Green或者打印枚舉對象時看到一堆反向鍵。原因數(shù)字枚舉編譯后生成正反雙向映射對象這是為了兼容舊代碼和反向查找場景。你刪了枚舉成員只要某個地方還引用了類型層面會報錯但運行時生成的對象是獨立于類型檢查的老的值可能還在。解決不需要反向映射就改用字符串枚舉或const enum。注意const enum在isolatedModules獨立編譯模式下有限制多人協(xié)作項目要提前在 tsconfig 里約定好避免某個人開了獨立編譯后構(gòu)建炸掉。5.3 strictNullChecks 開啟后的可選屬性連鎖反應(yīng)現(xiàn)象接口里寫color?: string函數(shù)里const c: string config.color直接編譯報錯。原因開啟 strict 后可選屬性的類型是string | undefined必須做收窄才能賦給純string變量。解決用if (config.color)判斷或者用config.color ?? 默認值提供兜底。更重要的教訓是團隊項目一開始就開 strict不要等項目寫到一半再開。我見過一個中期項目打開 strict 后冒出幾百個編譯錯誤全是這種可選屬性賦值問題排查成本遠超當初節(jié)省的那點時間。5.4 private 只是編譯期約束運行時一樣能訪問現(xiàn)象類的private age字段在另一個類里用(obj as any).age輕松拿值。原因TypeScript 的private是類型系統(tǒng)層面的編譯到 JavaScript 后字段就是普通屬性as any直接繞過類型檢查運行時沒有任何保護。解決需要運行時真正私有用 ES2022 的#age私有字段TypeScript 3.8 開始支持或者用 WeakMap 保存私有數(shù)據(jù)。如果只是在團隊內(nèi)部防誤用_前綴加 lint 規(guī)則也能起到提醒作用但那個只防君子不防小人。5.5 命名空間與模塊混用export namespace 的怪味道現(xiàn)象在 .ts 文件里寫export namespace Foo { export function bar() {} }導入時import { Foo } from ./foo能用但類型提示總是往深一層鉆編輯器自動補全也不友好。原因模塊本身已經(jīng)有作用域命名空間又包了一層產(chǎn)生兩層查詢路徑。類型提示和導入方都要多走一步API 表面被無意義地加深。解決模塊內(nèi)部一律不用命名空間直接 export 函數(shù)和類型。命名空間只保留給全局 .d.ts 聲明文件和純腳本場景其余地方出現(xiàn)export namespace基本可以斷定是代碼組織出了問題建議重構(gòu)成普通導出。6. 從教程到工程聲明合并、混入與 .d.ts 的進階驗證6.1 聲明合并與混入兩個低頻但關(guān)鍵的技巧聲明合并是教程里容易被跳過的章節(jié)但它是理解 .d.ts 聲明文件的基礎(chǔ)。同名接口會合并成員同名命名空間會合并導出命名空間還能和函數(shù)、類、枚舉合并。寫全局聲明時經(jīng)常需要靠聲明合并給已有的函數(shù)掛額外屬性比如給一個函數(shù)類型同時加 .version 和 .defaultOptions。混入解決的是多繼承缺失的問題。TypeScript 類只能單繼承但可以用展開混入把多個類的方法合并到一個目標類上。教程里那個例子核心是用Object.assign把多個原型上的方法拷貝過去再手動聲明合并后的類型。實際項目里混入用得不算多但遇到橫切關(guān)注點比如日志、鑒權(quán)比嵌套繼承更干凈。6.2 用 .d.ts 定義文件檢驗類型理解教程最后一章講編寫定義文件這是對整份教程最好的收尾驗證。一個合格的 .d.ts 要能描述參數(shù)對象、帶屬性的函數(shù)、既能 new 又能直接調(diào)用的方法、還有回調(diào)函數(shù)的形狀。寫一遍 .d.ts基本就把接口、類型推導、聲明合并、模塊導出這些知識點全部串起來了。我的習慣做法是拿一個無類型的 JavaScript 工具庫試著給它寫一份聲明文件再讓編譯器用--checkJs去驗證。聲明文件能通過編譯才算真正把這份教程里的類型設(shè)計思路消化了。從那以后每次學完一份 TypeScript 資料我都會強制自己走一遍寫聲明、補類型、跑檢查這個流程希望能幫到你。本文還有配套的精品資源點擊獲取