如何在項目中用好TypeScript

寫在最前:本文轉(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"
}

當鼠標懸浮在使用到該類型的地方時,編輯器會有更好的提示:


test1.png

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的使用非常簡單,直接用npmyarn安裝對應的聲明模塊即可。以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)。


test2.jpeg

在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,stringboolean,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分支里面,我們把收窄為neverfoo賦值給一個顯式聲明的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 善用高級類型

除了stringnumber、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)

TSextends關(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組件,可以訪問到很多的屬性
     })
  
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容