寫在最前:本文轉(zhuǎn)自掘金
1. 前言
- 我們都知,
JavaScript是一門非常靈活的編程語言,這種靈活也使他的代碼質(zhì)量參差不齊,維護成本高,運行時錯誤多。 -
TypeScript是添加了類型系統(tǒng)的JavaScript,適用于任何規(guī)模的項目,TS的類型系統(tǒng)很大程度上彌補了JS的缺點。 - 類型系統(tǒng)按照[類型檢查的時機]來分類,可以分為動態(tài)類型和靜態(tài)類型:
· 動態(tài)類型是指運行時才會進行類型檢查,這種語言的類型錯誤往往會導致運行時的錯誤,JS屬于動態(tài)類型,它是一門解釋型語言,沒有編譯階段。
· 靜態(tài)類型是只編譯階段就能確定每個變量的類型,這種語言的類型錯誤往往會導致語法錯誤。由于TS在運行前需要先編譯成JS,而在編譯階段就會進行類型檢查,所以TS屬于靜態(tài)類型。 - TS增強了編輯器的功能,包括代碼補全、接口提示、跳轉(zhuǎn)到定義、代碼重構(gòu)等,這在很大程度上提高了開發(fā)效率。TS的類型系統(tǒng)可以為大型項目帶來更高的可維護性,以及更少的bug。
- 為了提升開發(fā)幸福感,下面將詳細介紹如何在項目中用好TS。
2. 在項目中的實踐
2.1 善用類型注釋
- 我們可以通過
/** */形式的注釋給TS類型做標記提示:
/** person information**/
interface User{
name: string;
age: number;
sex: 'male' | 'female' ;
}
const p:User = {
name: "Lucky",
age:20,
sex:"female"
}
當鼠標懸浮在使用到該類型的地方時,編輯器會有更好的提示:

2.2 善用類型擴展
- TS 中定義類型有兩種方式:接口(
interface)和類型別名(type alias)。在下面的例子中,除了語法不一樣,定義的類型是一樣的:
// interface
interface Point{
x: number;
y: number;
}
interface SetPoint{
(x: number, y: number): void;
}
// type
type Point = {
x: number;
y: number;
}
type SetPoint = (x: number, y: number)=> void;
- 接口和類型別名均可以擴展:
// Interface extends interface
interface PartialPointX{
x: number;
}
interface Point extends PartialPointX{
y: number;
}
// Type alias extends type alias
type PartialPointX={
x:number;
}
type Point = PartialPointX & {y:number;};
- 接口和類型別名并不互斥的,也就是說,接口可以擴展類型別名,類型別名也可以擴展接口:
// Interface extends type alias
type PartialPointX={
x: number;
}
interface Point extends PartialPointX{
y: number;
}
// Type alias extends interface
interface PartialPointX{
x:number;
}
type Point = PartialPointX & {y:number;};
- 接口和類型別名的選用時機
· 在定義公共API(如編輯一個庫)時使用interface,這樣可以方便使用者繼承接口;
· 在定義組件屬性(Props)和狀態(tài)(State)時,建議使用type,因為type的約束性更強;
·type類型不能二次編輯,而interface可以隨時擴展。
2.3 善用聲明文件
- 聲明文件必須以
.d.ts為后綴。一般來說,TS會解析項目中所有的*.ts文件,因此也包含以.d.ts結(jié)尾的聲明文件。 - 只要
ts.config.json中的配置包含了typing.d.ts文件,那么其他的所有*.ts文件就都可以獲得聲明文件的類型定義。
2.3.1 第三方聲明文件
- 當在TS項目中使用第三方庫時,我們需要引用它的聲明文件,才能獲得對應的代碼補全、接口提示等功能。
- 針對多數(shù)第三方庫,社區(qū)已經(jīng)幫我們定義好了它們的聲明文件,我們可以直接下載下來使用。一般推薦使用
@types統(tǒng)一管理第三方庫的聲明文件,@types的使用非常簡單,直接用npm或yarn安裝對應的聲明模塊即可。以lodash為例:
npm install @types/lodash --save-dev
// or
yarn add @types/lodash --dev
2.3.2 自定義聲明文件
- 當一個庫沒有提供聲明文件,就需要我們自己寫聲明文件,以
antd-dayjs-webpack-plugin為例,當在config.ts中使用antd-dayjs-webpack-plugin時,若當編輯器沒有找到它的聲明文件,則會發(fā)生報錯 - 為了解決編輯器的報錯提示,我們可以采用它提供的另一種方法:添加一個包含
declare module 'antd-dayjs-webpack-plugin'; 的新聲明文件。我們也可以不用新增文件,在前面提到的typing.d.ts添加下面的內(nèi)容即可:
declare module 'antd-dayjs-webpack-plugin';
全局變量
當我們需要在多個ts文件中使用同一個typescript類型時,常見做法會在constant.ts文件中聲明相關(guān)類型,并將其export出去給其他ts文件import使用,無疑會產(chǎn)生很多繁瑣的代碼。前面我們提到,只要在tsconfig.json中配置包含了我們自定義的聲明文件*.d.ts,則聲明文件中的類型都能被項目中.ts文件獲取到。因此我們可以將多個ts文件都需要使用的全局類型卸載聲明文件中,需要使用該類型的ts文件不需要import就可以直接使用。
命名空間
在代碼量較大的情況下,為了避免各種變量名沖突,可將相同模塊的函數(shù)、類、接口等放置在命名空間內(nèi)。

在ts文件使用:
// src/views/Domain/index.ts
const cloumns: Domains.ListItem[] = []
...
// src/views/Department/index.ts
const columns: Departments.ListItem[] = []
2.4 善用 TypeScript 支持的JS新特性
2.4.1 可選鏈
let age = user && user.info && user.info.getAge // 寫法冗余且容易命中 `Uncaught TypeError: Cannot read property ...`
let age = user?.info?.getAge //如果其中有屬性不存在,會返回`null`或者`undefined`
2.4.2 空值合并運算符
當左側(cè)的操作數(shù)為null或者undefined時,返回其右側(cè)操作數(shù),否則返回左側(cè)操作數(shù)。
const user = {
level: 0,
}
let level1 = user.level ?? '暫無等級' // 0
let level2 = user.other_level ?? ''暫無等級' // 暫無等級
與||不同,或操作符為false值(例如,' '或0)時返回右側(cè)操作數(shù)
2.5 善用訪問限定
TS的類定義時允許使用private、protected、public三種訪問修飾符聲明成員訪問限制,并在編譯期間檢查,如果不加任何修飾符,默認為public訪問級別:
class Person {
private name: string;
private age: number;
// static 關(guān)鍵字可以將類里面的屬性和方法定義為類的靜態(tài)屬性和方法
public static sex: string = 'Male';
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
public run(): void {
console.log(this.name + '在跑步')
}
public setName(name:string): void {
this.name = name;
}
}
let p: Person = new Person('Tony', 22);
console.log(Person.sex); // Male
p.run(); //Tony在跑步
console.log(p.name) // name為私有屬性
2.6 善用類型收窄
TypeScript 類型收窄就是從寬類型轉(zhuǎn)換成窄類型的過程,其常用于處理聯(lián)合類型變量的場景。主要有以下方法收窄變量類型:
- 類型斷言
- 類型守衛(wèi)
- 雙重斷言
2.6.1 類型斷言
類型斷言可以明確地告訴TS值的詳細類型。其語法如下:
值 as 類型
// or
<類型>值
在 tsx 語法中必須使用前者。當TS不確定一個聯(lián)合類型的變量到底是哪個類型的時候,我們只能訪問此聯(lián)合類型的所有類型中共有的屬性和方法。
如何不完整的去實現(xiàn)接口結(jié)構(gòu)
例如,有一下接口,并被obj變量實現(xiàn),但賦值空對象會報錯
interface IStruct {
foo: string;
bar: {
barPropA: string;
barPropB: number;
barMethod: () => void;
baz: {
handler: () => Promise<void>;
};
};
}
const obj: IStruct = {} // 報錯
這個還是就可以使用類型斷言幫助實現(xiàn)先定義,后賦值,同時類型提示仍然存在:
const obj = <IStruct>{
}
需要注意的是,類型斷言只能夠欺騙TS編譯器,無法避免運行時的錯誤,反而濫用類型斷言可能會導致運行時錯誤
2.6.2 類型守衛(wèi)
類型守衛(wèi)主要有以下幾種方式:
- typeof :用于判斷
number,string,boolean,symbol四種類型; - instanceof:用于判斷一個實例是否屬于某個類;
- in:用于判斷一個屬性/方法是否屬于某個對象
typeof
可以利用 typeof 實現(xiàn)類型收窄和never 類型的特性做全面性檢查,如下面代碼所示:
type Foo = string | number
function controlFlowAnalysisWithNever(foo: Foo){
if(typeof foo === 'string'){
// 這里foo收窄為 string類型
}else if (typeof foo === 'number'){
// 這里foo 被收窄為number類型
} else {
// foo 在這里是never
const check: never = foo;
}
}
可以看到,在最后的else分支里面,我們把收窄為never的foo賦值給一個顯式聲明的never變量,如果一切邏輯正確,那么是能夠編譯通過。假如某天某人修改了Foo的類型,而忘記修改controlFlowAnalysisWithNever方法中的控制流程,這時候else分支的foo類型無法賦值給never類型,產(chǎn)生一個編譯錯誤。通過使用never避免出現(xiàn)新增了聯(lián)合類型沒有對應的實現(xiàn),確保了方法總是窮盡Foo的所有類型,從而保證代碼的安全性。
但如果我們將typeof判斷邏輯提取到函數(shù)外部進行復用
function isString(input: unknown): boolean {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 類型“string | number”上不存在屬性“replace”。
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
奇怪的事情發(fā)生了,如果 isString 返回了 true ,那input肯定是string類型。但因為TS 無法做到跨函數(shù)上下文來進行類型的信息收集判斷。為了彌補該項的不足,TS引入了is關(guān)鍵字來顯式地提供類型信息:
function isString(input: unknown): input is string {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 正確了
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
isString 函數(shù)稱為類型守衛(wèi),在它的返回值中,我們不再使用 boolean 作為類型標注,而是使用 input is string 這么個奇怪的搭配,拆開來看它是這樣的:
- input 函數(shù)的某個參數(shù);
- is string,即 is 關(guān)鍵字 + 預期類型,即如果這個函數(shù)成功返回為
true,那么is關(guān)鍵字前這個入?yún)⒌念愋?,就?strong>被這個類型守衛(wèi)調(diào)用方后續(xù)的類型控制流分析收集到。
注意,類型守衛(wèi)函數(shù)中并不會對判斷邏輯和實際類型的關(guān)聯(lián)進行檢查:
// 只會顯式的認為 input is number
function isString(input: unknown): input is number {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 報錯,在這里變成了 number 類型
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
從這個角度來看,其實類型守衛(wèi)有些像類型斷言,但是類型守衛(wèi)更寬容,更信任你一些。你指定什么類型,它就是什么類型。
這里提供開發(fā)中兩個常用的兩個類型守衛(wèi):
export type Falsy = false | "" | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val;
// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;
export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);
instanceof
使用instanceof運算符收窄變量的類型:
class Man {
handsome = "handsome";
}
class Women{
beautiful = "beautiful"
}
function Human(arg: Man | Woman){
if(arg instanceof Man){
console.log(arg.handsome)
}else{
console.log(arg.beautiful)
}
}
in
使用in做屬性檢查
interface A{
a: string;
}
interface B{
b: string;
}
function foo(x: A | B){
if("a" in x){
return x.a;
}
return x.b
}
2.6.3 雙重斷言
當我們要為某個值作類型斷言時,我們需要確保編輯器推斷出的值的類型和新類型有重疊,否則,無法簡單地作類型斷言,任何類型都可以被斷言為any,而any可以被斷言為任何類型。
如果我們?nèi)匀幌胧褂媚莻€類型,可以使用雙重斷言
function handler(event: Event){
const element = event as any as HTMLElement;
}
TS3.0中新增了一種unknown類型,它是一種更加安全的any的副本。所有東西都可以被標記成是unknown類型,但是unkonwn必須在進行類型判斷和條件控制之后才可以被其他類型,并且在類型判斷和條件控制之前也不能進行任何操作
2.7 善用常量枚舉
2.8 善用高級類型
除了string、number、boolean這種基礎類型外,我們還應該了解一些類型聲明中的一些高級用法。
2.8.1 類型索引(keyof)
keyof 類似于Object.keys,用于獲取一個接口中key的聯(lián)合類型:
interface Button{
type: string;
text: string;
}
type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"
2.8.2 類型約束(extends)
TS中extends關(guān)鍵詞不同于在Class后使用extends的繼承作用,一般在泛型內(nèi)使用,它主要作用是對泛型加以約束:
type BaseType = string | number | boolean // 這里表示copy 的參數(shù)
function copy<T extends BaseType>(arg: T):T{
return arg
}
const arr = copy([]) // error
extends經(jīng)常和keyof一起使用,例如我們有一個getValue方法專門獲取對象的值,但是這個對象并不確定,我們就可以這樣做:
function getValue<T, K extends keyof T>(obj: T, key: K){
return obj[key]
}
const obj = {a:1}
const a = getValue(obj, 'b') //error
當傳入對象沒有key時,編輯器則會報錯
2.8.3 類型映射(in)
in關(guān)鍵詞的作用主要是做類型的映射,遍歷已有接口的key或者是遍歷聯(lián)合類型。以內(nèi)置的泛型接口Readonly為例,它的實現(xiàn)如下:
type Readonly<T> ={
readonly [P in keyof T]: T[P];
}
// 它的作用是將所有接口變?yōu)橹蛔x
interface Obj {
a: string;
b: string;
}
type ReadOnlyObj = Readonly<Obj>
//等效于
interface Obj {
readonly a: string;
readonly b: string;
}
2.8.3 條件類型(U?X:Y)
條件類型的語法規(guī)則和三元表達式一致,經(jīng)常用于類型不確定的情況
T extends U ? X : Y
上面的意思就是,如果 T 是 U 的子集,就是類型 X,否則為類型 Y。以內(nèi)置的泛型接口 Extract 為例,它的實現(xiàn)如下:
type Extract<T,U> = T extends U ? T : never;
TypeScript 將使用 never類型來表示不應該存在的狀態(tài)。上面的意思是,如果 T 中的類型在 U 存在,則返回,否則拋棄。
假設我們兩個類,有三個公共的屬性,可以通過Extract提取這三個公共屬性:
interface Worker{
name: string;
age: number;
email:string;
salary: number;
}
interface Worker{
name: string;
age: number;
email:string;
grade: number;
}
type CommonKeys = Extract<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'
2.8.4工具泛型
TS 中內(nèi)置了很多工具泛型,前面介紹過Readonly、'Extract' 這兩種,內(nèi)置的泛型在TS內(nèi)置的lib.es5.d.ts中都有定義,所以不需要任何依賴就可以直接使用。下面介紹幾個常見的工具泛型的作用和使用方法。
Exclude,如果T中的類型在U不存愛,則返回,否則拋棄。
interface Worker{
name: string;
age: number;
email:string;
salary: number;
}
interface Worker{
name: string;
age: number;
email:string;
grade: number;
}
type CommonKeys = Exclude<keyof Worker, keyof Student>
// 'salary'
Partial 用于將一個接口所有屬性設置為可選狀態(tài):
interface Person {
name: string;
sex: string;
}
type NewPerson = Partial<Person>
// {name?:string;sex?:string}
Required的作用是將所有接口可選屬性改為必選的
interface Person {
name?: string;
sex?: string;
}
type NewPerson = Required<Person>
// {name:string;sex:string}
Pick主要作用提取接口的某幾個屬性:
interface Todo{
title: string;
completed: boolean;
description: string
}
type TodePrevied = Pick<Tode, "title"|"completed">
// {title: string;completed:boolean}
Omit的作用是剔除接口的某幾個屬性
interface Todo{
title: string;
completed: boolean;
description: string
}
type TodePrevied = Omit<Tode, "description">
// {title: string;completed:boolean}
2.8.5 工具泛型獲取組件實例的類型
InstanceType<T> 是 ts 自帶的類型, 能夠直接獲取組件完整的實例類型
import Child from './child.vue'
import {ElImage} from 'element-plus'
type ElImageCtx = InstanceType(typeof ElImage);
type ChildCtx = InstanceType(typeof Child);
...
setup() {
const child = ref<null | ChildCtx>(null);
const elImgRef = ref<null | ElImageCtx>(null)
onMounted(() => {
child.value?.num ;// 可以直接訪問到
elImgRef.value?. // 對于 element組件,可以訪問到很多的屬性
})
}