這節課老師講了 TypeScript 的用處與基本語法、高級類型的應用、類型保護與類型守衛
什麼是 TypeScript#
發展歷史#
- 2012-10:微軟發布了 TypeScript 第一個版本 (0.8)
- 2014-10:Angular 發布了基於 TypeScript 的 2.0 版本
- 2015-04:微軟發布了 Visual Studio Code
- 2016-05:@ ty pes/react 發布,TypeScript 可開發 React
- 2020-09:Vue 發布了 3.0 版本,官方支持 TypeScript
- 2021-11:v4.5 版本發布
為什麼是 TypeScript#
動態類型在執行過程中進行類型的匹配,js 的弱類型會在執行時進行隱式類型轉換,而在靜態類型中則不然
TypeScript 則為靜態類型:java、c/c++ 等
- 可讀性增強:基於語法解析 TSDoc,ide 增強
- 可維護性增強:在編譯階段暴露大部分錯誤
- 多人合作的大型項目中,可以獲得更好的穩定性和開發效率
TypeScript 是JS 的超集
- 包含於兼容所有 Js 特性, 支持共存
- 支持漸進式引入與升級
基本語法#
基本數據類型#
js ==> ts
可以看到,ts 的類型定義方式:let 變量名: 類型 = 值;
對象類型#
// 創建一個對象,包括以下屬性,類型為IBytedancer
// I表示自定義的一個類型(一個命名約定),與類和對象進行區分
const bytedancer: IBytedancer = {
jobId: 9303245,
name: 'Lin',
sex: 'man',
age: 28,
hobby: 'swimming',
}
// 定義一個類型為IBytedancer
interface IBytedancer {
/* 只讀屬性readonly:約束屬性不可在對象初始化外賦值 */
readonly jobId: number;
name: string;
sex: 'man' | 'woman' | 'other';
age: number;
/* 可選屬性:定義該屬性可以不存在 */
hobby?: string;
/* 任意屬性:約束所有對象屬性都必須是該屬性的子類型 */
[key: string]: any; // any 任何類型
}
/* 報錯:無法分配到"jobId",因為它是只讀屬性 */
bytedancer.jobId = 12345;
/* 成功:任意屬性標註下可以添加任意屬性 */
bytedancer.plateform = 'data';
/* 報錯:缺少屬性"name", 而hobby可缺省 */
const bytedancer2: IBytedancer = {
jobId: 89757,
sex: "woman",
age: 18,
}
函數類型#
js:
function add(x, y!) {
return x + y;
}
const mult = (x, y) => x * y;
function add(x: number, y: number): number {
return x + y;
}
const mult: (x: number, y: number) => number = (x, y) => x * y;
// 簡化寫法,定義接口IMult
interface IMult {
(x: number, y: number): number ;
}
const mult: IMult = (x, y) => x * y;
可以看到,格式為function 函數名(參數:類型...):返回值類型
函數重載#
/* 對getDate函數進行重載,timestamp為可缺省參數 */
function getDate(type: 'string', timestamp?: string): string;
function getDate(type: 'date', timestamp?: string): Date;
function getDate(type: 'string' | 'date', timestamp?: string): Date | string {
const date = new Date(timestamp);
return type === 'string' ? date.toLocaleString() : date;
};
const x = getDate('date'); // x: Date
const y = getDate('string', '2018-01-10'); // y: string
簡化形式如下:
interface IGetDate {
(type : 'string', timestamp ?: string): string; // 這個地方返回類型改為any就可以通過了
(type : 'date', timestamp?: string): Date;
(type: 'string' | 'date', timestamp?: string): Date | string;
}
/* 報錯:不能將類型"(type: any, timestamp: any) => string | Date"分配給類型"IGetDate"。
不能將類型"string | Date" 分配給類型"string"。
不能將類型 "Date"分配給類型"string"。ts(2322) */
const getDate2: IGetDate = (type, timestamp) => {
const date = new Date(timestamp);
return type === 'string' ? date.toLocaleString() : date;
}
陣列類型#
type作用就是給類型起一個新名字,相當於 c++ 中的 typedef
/* 「類型+方括號」表示 */
type IArr1 = number[];
/* 泛型表示 這兩種最常用*/
type IArr2 = Array<string | number| Record<string, number> > ;
/* 元組表示 */
type IArr3 = [number, number, string, string];
/* 接口表示 */
interface IArr4 {
[key: number]: any;
}
const arrl: IArr1 = [1, 2, 3, 4, 5, 6];
const arr2: IArr2 = [1, 2, '3', '4', { a: 1 }];
const arr3: IArr3 = [1, 2, '3', '4'];
const arr4: IArr4 = ['string', () => null, {}, []];
TypeScript 補充類型#
- 空類型:表示無賦值
- 任意類型:是所有類型的子類型
- 枚舉類型:支持枚舉值到枚舉名的正、反向映射
/* 空類型,表示無賦值 */
type IEmptyFunction = () => void;
/* 任意類型,是所有類型的子類型 */
type IAnyType = any;
/* 枚舉類型:支持枚舉值到枚舉名的正、反向映射 */
enum EnumExample {
add = '+',
mult = '*',
}
EnumExample['add'] === '+';
EnumExample['+'] === 'add';
enum ECorlor { Mon, Tue, Wed, Thu, Fri, Sat, Sun };
ECorlor['Mon'] === 0;
ECorlor[0] === 'Mon' ;
/*泛型*/
type INumArr = Array<number>;
Typescript 泛型#
泛型,之前學過 c++ 的話 dddd,跟 c++ 中的差不多:不預先指定具體的類型,而在使用的時候再指定類型的一種特性
function getRepeatArr(target) {
return new Array(100).fill(target);
}
type IGetRepeatArr = (target: any) => any[];
/* 不預先指定具體的類型,而在使用的時候再指定類型的一種特性 */
type IGetRepeatArrR = <T>(target: T) => T[];
泛型還可以使用在以下場景中:
/*泛型接口&多泛型*/
interface IX<T, U> {
key: T;
val: U;
}
/* 泛型類 */
class IMan<T> {
instance: T;
}
/* 泛型別名 */
type ITypeArr<T> = Array<T>;
泛型還可以進行約束範圍
/* 泛型約束:限制泛型必須符合字符串 */
type IGetRepeatStringArr = <T extends string>(target: T) => T[];
const getStrArr: IGetRepeatStringArr = target => new Array(100).fill(target);
/* 報錯:類型"number"的參數不能賦給類型“string"的參數 */
getStrArr(123) ;
/* 泛型參數默認類型 */
type IGetRepeatArr<T = number> = (target: T) => T[];// 與結構中的默認賦值有點類似
const getRepeatArr: IGetRepeatArr = target => new Array(100).fill(target);// 這裡的IGetRepeatArr就是一個類型別名,此處沒有傳參數給這個類型別名
/* 報錯:類型"string"的參數不能賦給類型“numbe r"的參數 */
getRepeatArr('123');
類型別名 & 類型斷言#
類型斷言#
有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細信息。 通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言 這種方式可以告訴編譯器,“相信我,我知道自己在幹什麼”。 類型斷言好比其它語言裡的類型轉換,但是不進行特殊的數據檢查和解構。 它沒有運行時的影響,只是在編譯階段起作用。 TypeScript 會假設你,程序員,已經進行了必須的檢查。
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
/*通過type關鍵字定義了IObjArr的別名類型*/
type IObjArr = Array<{
key: string;
[objKey: string]: any;
}>
function keyBy<T extends IObjArr>(objArr: Array<T>) {
/* 未指定類型時,result類型為{} */
const result = objArr.reduce((res, val, key) => {
res[key] = val;
return res;
}, {});
/* 透過as關鍵字,斷言result類型為正確類型 */
return result as Record<string, T>;
}
上述代碼,中有幾個點需注意:
reduce() 函數對數組中的每個元素執行一個由您提供的reducer函數 (升序執行),將其結果匯總為單個返回值。
語法:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
字符串 / 數字 字面量#
/* 允許指定字符 串/數字必須的固定值*/
/* IDomTag必須為html、body、div、 span中的其一*/
type IDomTag = 'html' | ' body' | 'div' | 'span';
/* IOddNumber必須為1、 3、5、7、9中的其一 */
type IOddNumber = 1 | 3 | 5 | 7 | 9;
高級類型#
聯合 / 交叉類型#
為書籍列表編寫類型 -> ts 類型聲明繁瑣存在較多重複。高級類型
const bookList = [ { // 普通js
author:'xiaoming',
type:'history',
range: '2001 -2021',
}, {
author:'xiaoli',
type:'Story',
theme:'love',
}]
// ts 繁瑣
interface IHistoryBook {
author:String;
type:String;
range:String
}
interface IStoryBook {
author:String;
type:String;
theme:String;
}
type IBookList = Array<IHistoryBook | IStoryBook>;
- 聯合類型: IA | IB; 聯合類型表示一個值可以是幾種類型之一
- 交叉類型: IA & IB; 多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性
上述代碼可以通過 ts 簡化為:
type IBookList = Array<{
author: string;
} & ({
type: 'history';
range: string;
} | {
type: 'story';
theme: string;
})>;
/* 限制了author只能為string類型,而type只能'history'/'story'二選一,並且type不同可能的屬性不同 */
類型保護與類型守衛#
- 訪問聯合類型時,處於程序安全,僅能訪問聯合類型中的交集部分
interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }
function log(arg: IA | IB) {
/*報錯:類型"IA | IB" 上不存在屬性"a”。 類型"IB"上不存在屬性"a"
結論:訪問聯合類型時,處於程序安全,僅能訪問聯合類型中的交集部分*/
if(arg.a) {
console.log(arg.a1);
} else {
console.log(arg.b1);
}
}
上述報錯可通過類型守衛解決:定義一個函數,其返回值是一个類型謂詞,生效範圍為子作用域
interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }
/*類型守衛:定義一個函數,。它的返回值是一个類型謂詞,生效範圍為子作用域 */
function getIsIA(arg: IA | IB): arg is IA {
return !!(arg as IA).a;
}
function log2(arg: IA | IB) {
/* 不存在報錯了 */
if(getIsIA(arg) ) {
console.log(arg.a1);
} else {
console.log(arg.b1);
}
}
或者 typeof 和 instance 判斷
// 實現函數reverse 可將數組或字符串進行反轉
function reverse(target: string | Array<any>) {
/* typof 類型保護*/
if (typeof target === 'string') {
return target.split('').reverse().join('');
}
/* instance 類型保護*/
if (target instanceof Object) {
return target.reverse() ;
}
}
不會每次都這麼麻煩吧,事實上,只有當兩個類型沒有任何重合點的話才需要類型守衛,如上述的書本例子,可以進行自動類型推斷。
// 實現函數logBook類型
// 函數接受書本類型,並logger出相關特徵
function logBook(book: IBookItem) {
// 聯合類型+類型保護=自動類型推斷
if (book.type === 'history'){
console.log(book.range)
} else{
console.log(book.theme);
}
}
再來看一個 case,實現一個子集不污染的合併函數 merge,將 sourceObj 合併到 targetObj 中,sourceObj 必須為 targetObj 的子集
function merge1(sourceObj, targetObj) { // js中,實現複雜,這樣才能不污染
const result = { ...sourceObj };
for(let key in targetObj) {
const itemVal = sourceObj[key];
itemVal && ( result[key] = itemVal );
}
return result;
}
function merge2(sourceObj, targetObj) {// 若這兩個入參的類型沒問題,則可以這樣
return { ...sourceObj, ...targetObj };
}
而一種簡單的思想就是在 ts 中編寫兩個類型,進行判斷,但這樣又會存在實現繁瑣,增加 target 需要 source 聯動去除,重複維護了兩份 x、y
interface ISource0bj {
x?: string;
y?: string;
}
interface ITarget0bf {
x: string;
y: string;
}
type IMerge = (source0bj: ISource0bj, target0bj: ITarget0bj) => ITargetObj;
/* 類型實現繁瑣:若obj類型較為複雜,則聲明source和target便需要大量重複2遍
容易出錯:若target增加/減少key,則需要source聯動去除 */
通過泛型,改進,這裡涉及到幾個個知識點
- Partial:一個常見的任務是將一個已知的類型每個屬性都變為可選的
TypeScript 提供了從舊類型中創建新類型的一種方式 ——映射類型。 在映射類型裡,新類型以相同的形式去轉換舊類型裡每個屬性。 (直接寫就行,ts 內置了)
- 關鍵字keyof,其相當於取值對象中的所有 key 組成的字符串字面量
- 關鍵字in,其相當於取值字符串字面量中的一種可能,配合泛型 P, 即表示每個 key
- 關鍵字 ? ,通過設置對象可選選項,即可自動推導出子集類型
interface IMerge {
<T extends Record<string, any>>(sourceObj: Partial<T>, targetObj: T): T;
}
// Partial內部實現
type IPartial<T extends Record<string, any>> = {
[P in keyof T]?:T[P];
}
// 索引類型:關鍵字[keyof] ,其相當於取值對象中的所有key組成的字符串字面量,如
type IKeys = keyof{a: string; b: number }; // => type IKeys ="a" | "b"
// 關鍵字[in],其相當於取值 字符串字面量中的一種可能,配合泛型P, 即表示每個key
// 關鍵字[ ? ],通過設置對象 可選選項,即可自動推導出子集類型
函數返回值類型#
函數返回值類型在定義時候是不明確的,也應該通過泛型進行表達
下文代碼 delayCall 接受一個函數作為入參,其實現延遲 1s 運行函數 func,其返回 promise,結果為入參函數的返回結果
// 如何實現函數delayCall的類型聲明
// delayCall接受一個函數作為入參,其實現延遲1s運行函數
// 其返回promise,結果為入參函數的返回結果
function delayCall(func) {
return new Promisd(resolve => {
setTimeout(() => {
const result= func );
resolve(result);
},1000);
});
}
-
關鍵字 extends 跟隨泛型出現時,表示類型推斷,其表達可類比三元表達式
- 如
T === 判斷類型?類型A:類型B
->T extends 判斷類型?類型A:類型B
- 如
-
關鍵字 infer 出現在類型推薦中,表示定義類型變量,可以用於指代類型
infer 簡單示例如下:
type ParamType<T> = T extends (...args: infer P) => any ? P : T;
在這個條件語句
T extends (...args: infer P) => any ? P : T
中,infer P
表示待推斷的函數參數。整句表示為:如果
T
能賦值給(...args: infer P) => any
,則結果是(...args: infer P) => any
類型中的參數P
,否則返回為T
。- 在這裡就相當於把這個函數返回值類型指代為 R
type IDelayCall= <T extends () => any>(func: T) => ReturnType<T>;
type IReturnType<T extends (...args: any) => any> = T extends(...args: any ) => infer R ? R : any
// 關鍵字[extends] 跟隨泛型出現時,表示類型推斷,其表達可類比三元表達式
// 如T === 判斷類型?類型A:類型B
// 關鍵字[infer] 出現在類型推薦中,表示定義類型變量,可以用於指代類型
// 如該場景下,將函數的返回值類型作為變量,使用新泛型R表示,使用在類型推薦命中的結果中
工程應用#
TypeScript 工程應用 ——Web#
- 配置 webapack loader相關配置
- 配置 tsconfig.js文件(寬鬆 —— 嚴格,都可以定義)
- 運行 webpack啟動 / 打包
- loader 處理 ts 文件時, 會進行編譯與類型檢查
相關 loader:
TypeScript 工程應用 ——Node#
使用 TSC 編譯
- 安裝 Node 與 npm
- 配置 tsconfig.js 文件
- 使用 npm 安裝 tsc
- 使用 tsc 運行編譯得到 js 文件
總結感想#
這節課老師講了 TypeScript 的用處與基本語法、和 JS 的對比、高級類型的應用,後續也深入講了一下類型保護與類型守衛,在最後總結了 TypeScript 如何在工程中進行應用。TypeScript 作為 JS 的一個超集,他增加了類型檢查的功能,可以在編譯階段就將代碼中的錯誤暴露出來,這是 js 這類動態類型所不具備的,在多人合作的大型項目中,使用 TS 往往可以獲得更好的穩定性和開發效率。
本文引用的大部分內容來自林皇老師的課以及 ts 官方文檔~