前端 Typescript 入門
Ant design vue4.x 基於 vue3,示例預設是 TypeScript。比如 table 元件管理。
vue3 官網介紹也使用了 TypeScript,例如:響應式 API:核心
華為的鴻蒙OS(HarmonyOS)開發中也可以使用 TypeScript
本篇目的用於對 TS 進行掃盲
Tip:ts 路線圖
ts 是什麼
TS是TypeScript的縮寫,由微軟開發的一種開源的程式語言
以前官網說“ts 是 js 超級”,現在改為: TypeScript是具有型別語法的JavaScript。
目前 TypeScript 5.4 已經發布(2024-03) —— ts 官網
Tip:ts缺點:開發更費麻煩,要多寫東西了,看個人取捨。
環境
基於筆者博文《vue3 入門》,就像這樣:
<template>
<section>
</section>
</template>
<script lang="ts" setup name="App">
// ts
</script>
<style>
</style>
也可以直接在ts線上執行環境進行。
推導型別和顯示註解型別
TS = 型別
+ javascript
ts 編譯過程:
- TypeScript原始碼 -> TypeScript AST
型別檢查器
檢查AST- TypeScript AST -> JavaScript 原始碼
顯示註解型別,語法:value:type
告訴型別檢查器,這個 value 型別是 type。請看示例:
<template>
<p>{{ a }}</p>
<p>{{ b }}</p>
<p>{{ c }}</p>
</template>
<script lang="ts" setup name="App">
// 顯示註解型別
let a: number = 1 // a 是數字
let b: string = 'hello' // b 是字串
let c: boolean[] = [true, false]; // 布林型別陣列
</script>
如果將 a 寫成 let a: number = '3'
,vscode 中 a 就會出現紅色波浪,移上去會看到提示:不能將型別“string”分配給型別“number”。
如果想讓 typescript 推到型別,就去掉註解,讓 ts 自動推導。就像這樣:
// 推導型別
let a = 1 // a 是數字
let b = 'hello' // b 是字串
let c = [true, false]; // 布林型別陣列
去掉註解後,型別並沒有變。並且如果嘗試修改 a 的型別,ts 也會報錯。就像這樣:
let a = 1 // a 是數字
// 嘗試替換成字串,vscode 會提示:不能將型別“boolean”分配給型別“number”。
a = true
Tip:有人說“最好讓 ts 推導型別,少數情況才需要顯示註解型別”。
另外雖然大量錯誤 ts 在編譯時無法捕獲,例如堆疊溢位、網路斷連,這些屬於執行時異常。ts 能做的是將 js 執行時的報錯提到編譯時。比如以下程式碼:
const obj = { width: 10, height: 15 };
// 提示 heigth 屬性寫錯了
const area = obj.width * obj.heigth;
let a = 1 + 2
let b = a + 3
// 滑鼠以上 c,可以看到 c 對應的型別
let c = {
apple: a,
banana: b
}
型別斷言
請看示例:
let arr = [1, 2, 3]
// r 為3
const r = arr.find(item => item > 2)
// “r”可能為“未定義”。ts(18048)
// const r: number | undefined
r * 5 // {1}
行 {1} 處的 r 會錯誤提示,說 r 可能會是 undefined。
要解決這個問題,可以使用型別斷言
:用來告訴編譯器一個值的具體型別,當開發者比編譯器更瞭解某個值的具體型別時,可以使用型別斷言來告訴編譯器應該將該值視為特定的型別。
型別斷言有兩種形式,分別是尖括號語法
和 as 語法
。這裡說一下 as 語法:value as type
。請看示例:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
上述示例改成這樣,r 就不會報錯了。
// 告訴編譯器,r 一定是一個 number
const r = arr.find(item => item > 2) as number
r * 5
Tip:由於需要人為干預,所以使用起來要謹慎
基礎型別
js 基本型別大概有這些:
let num1 = 10;
let str1 = 'Hello';
let isTrue = true;
let undefinedVar;
let nullVar = null;
const symbol1 = Symbol('description');
const bigIntNum = 9007199254740991n;
let notANumber = NaN;
let infinite = Infinity;
console.log(typeof num1); // number
console.log(typeof str1,); // string
console.log(typeof isTrue); // boolean
console.log(typeof undefinedVar); // undefined
console.log(typeof nullVar); // object
console.log(typeof symbol1); // symbol
console.log(typeof bigIntNum); // bigint
console.log(typeof notANumber); // number
console.log(typeof infinite); // number
ts 中基本型別有:
// let v1: String = 'a' - 大寫 String 也可以
let v1: string = 'a'
let v2: number = 1
let v3: boolean = true
let v4: null = null
let v5: undefined = undefined
// 字串或者null
let v6: string | null = null
// 錯誤:不能將型別“5”分配給型別“1 | 2 | 3”
let v7: 1 | 2 | 3 = 5
// 正確
let v8: 1 | 2 | 3 = 2
聯合型別
陣列
ts 陣列有兩種方法,看個人喜好即可。請看示例:
// 方式一
// 定義一個由數字組成的陣列
let arr1: number[] = [2, 3, 4]
// 報錯:不能將型別“string”分配給型別“number”
let arr2: number[] = [2, 3, 4, '']
// 方式二
let arr3: Array<string> = ['a', 'b', 'c']
// 報錯:不能將型別“number”分配給型別“string”。
let arr4: Array<string> = ['a', 'b', 'c', 4]
元組
在 TypeScript 中,元組(Tuple)是一種特殊的陣列型別,它允許您指定一個固定長度和對應型別的陣列
let arr5:[string, number, string] = ['a', 1, 'b']
// 報錯:不能將型別“[string, number]”分配給型別“[string, number, string]”。源具有 2 個元素,但目標需要 3 個。
let arr6:[string, number, string] = ['a', 1]
// 正確
arr6[0] = 'a2'
// 錯誤:不能將型別“number”分配給型別“string”。
arr6[0] = 1
// 第三個新增 ? 表明可選,這樣只傳入 2 個數也不會報錯
let arr7:[string, number, string?] = ['a', 1, 'b']
列舉
列舉需要使用關鍵字 enum。請看示例:
// 就像定義物件,不過不需要 =
enum TestEnum {
a,
b,
c,
}
// 1
console.log(TestEnum.b);
// b
console.log(TestEnum[1]);
// string
console.log(typeof TestEnum[1]);
ts 可以自動為列舉型別中的各成員推導對應數字。上面示例推導結果:
enum TestEnum {
a = 0,
b = 1,
c = 2,
}
也可以自己手動設定:
enum TestEnum2 {
a = 3,
b = 13,
c = 23,
}
// 13
console.log(TestEnum2.b);
比如這個,c 就是 b 的下一個數字:
enum TestEnum3 {
a,
b = 13,
c,
}
// 14
console.log(TestEnum3.c);
使用場景
:比如你之前根據訂單狀態寫了如下程式碼,可以用列舉來增加可讀性。
if(obj.state === 0){
}else if(obj.state === 1){
}else if(obj.state === 2){
}else if(obj.state === 3){
}
// 最佳化後
enum 訂單狀態{
取消,
上線,
傳送,
退回,
...
}
if(obj.state === 訂單狀態.取消){
}else if(obj.state === 訂單狀態.上線){
}else if(obj.state === 訂單狀態.傳送){
}else if(obj.state === 訂單狀態.退回){
}
函式
定義一個函式,引數報錯:
// 引數 a 和 b報錯。例如:a - 引數“a”隱式具有“any”型別。
function fn1(a, b){
return a + b
}
定義引數型別:
function fn2(a: number, b : number){
return a + b
}
定義引數 b 可選,返回值是 number型別。請看示例:
// b是可選。
// 必選的放左側,可選的放後側
function fn5(a: number, b?: number): number{
return 10
}
// 應有 1-2 個引數,但獲得 0 個。
fn5()
定義引數 a 的預設值,rest是一個字串陣列:
// a 有一個預設值 10
function fn7(a = 10, b?: number, ...rest:string[]): number{
return 10
}
fn7(1,2, 'a', 'b')
void
通常用於函式,表示沒有 return 的函式。
function fn3(a: number, b : number):void{
// 不能將型別“number”分配給型別“void”。
return a + b
}
function fn4(a: number, b : number): void{
}
介面
通常用於物件的定義。請看示例:
interface Person{
name: string,
age: number
}
const p: Person = {
name: 'peng',
age: 18
}
// 報錯:型別 "{ name: string; }" 中缺少屬性 "age",但型別 "Person" 中需要該屬性。ts(2741)
const p2: Person = {
name: 'peng',
}
型別別名
比如定義了一個變數 v1,其型別可以是 number 或 string,但是好多地方都是這個型別:
let v1: number | string = 3
我們可以透過 type
定義一個別名
。就像這樣:
// 定義別名 Message
type Message = number | string
let v2: Message = 'hello'
// 報錯:不能將型別“boolean”分配給型別“Message”
let v3: Message = true
泛型
比如定義如下一個處理 number 的函式:
function fn1(a: number, b:number): number[]{
return [a, b]
}
假如以後想把這個函式作為一個通用函式,除了可以處理 number,還可以處理 string 等其他型別,比如:
function fn1(a: string, b:string): string[]{
return [a, b]
}
a: string | number
又交叉了。就像這樣:
function fn1(a: string | number, b:string | number): string[]{
return [a, b]
}
這裡可以使用泛型
,請看示例:
// 定義一個變數,比如 T
function fn1<T>(a: T, b:T): T[]{
return [a, b]
}
fn1<number>(11, 11)
fn1<string>('a', 'a')
// 正確,ts 會自動推導
fn1('a', 'a')
再看一個泛型示例:
// 引數 arr 是 T 型別的陣列
// 返回 T 型別或 undefined
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
firstElement(['a', 'b'])
函式過載
java 中函式過載是定義多個方法,呼叫時根據引數型別
和數量
的不同執行不同的方法。例如下面定義兩個 add:
// 方法過載示例:兩個引數的相加
public int add(int a, int b) {
return a + b;
}
// 方法過載示例:三個引數的相加
public int add(int a, int b, int c) {
return a + b + c;
}
ts 這裡過載和 java 中的有些不同,可以稱之為函式過載申明
。
比如首先我們寫了一個數字相加
或字串相加
的方法:
// 數字相加
// 字串相加
function combine(x: number | string, y: number | string): number | string {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
} else if (typeof x === 'string' && typeof y === 'string') {
return x + y;
}
// 處理其他情況
return 'Invalid input';
}
console.log(combine(1, 2)); // 輸出:3
console.log(combine('hello', 'world')); // 輸出:helloworld
這裡有兩個問題:
// 問題一:滑鼠移動到 combine 顯示:
// function combine(x: number | string, y: number | string): number | string
console.log(combine(1, 2));
console.log(combine('hello', 'world'));
// 問題二:傳入 number和 string 不合法,但不報錯。滑鼠移動到 combine 顯示:
// function combine(x: number | string, y: number | string): number | string
console.log(combine(1, 'two')); // 輸出:Invalid input
現在加上函式過載申明
,就能解決上述兩個問題。請看示例:
// 函式過載
function combine(x: number, y: number): number;
// 變數名可以不是x、y
function combine(x2: string, y2: string): string;
function combine(x: number | string, y: number | string): number | string {
// 不變
}
// function combine(x: number, y: number): number (+1 overload)
console.log(combine(1, 2));
// function combine(x: string, y: string): string (+1 overload)
console.log(combine('hello', 'world'));
// 報錯:沒有與此呼叫匹配的過載。
// 第 1 個過載(共 2 個),“(x: number, y: number): number”,出現以下錯誤。
// 第 2 個過載(共 2 個),“(x: string, y: string): string”,出現以下錯誤。ts(2769)
console.log(combine(1, 'two'))
介面繼承
直接看示例:
interface Person{
name: string,
age: number
}
// Student 繼承 Person
interface Student extends Person{
school: string
}
// 提示p缺少3個屬性
// 型別“{}”缺少型別“Student”中的以下屬性: school, name, agets(2739)
const p: Student = {
}
Student 繼承 Person,有了3個屬性。
類的修飾符
類的修飾符有:public、private、protected、static、readonly...。用法請看下文:
比如有這樣一段正常的js程式碼:
class People{
constructor(name){
this.name =name;
}
// 不需要逗號
sayName(){
console.log(this.name)
}
}
let people = new People('aaron')
people.sayName() // aaron
放在 ts(比如 ts線上執行環境) 中會報錯如下:
Parameter 'name' implicitly has an 'any' type.
Property 'name' does not exist on type 'People'.
Property 'name' does not exist on type 'People'.
需要修改如下兩處即可消除所有錯誤:
class People{
- constructor(name){
+ // 消除ts報錯:型別“People”上不存在屬性“name”
+ name: string
+ constructor(name: string){
this.name =name;
}
// 不需要逗號
其中 name: string
的作用:宣告 People 類有個必填屬性。例項化 People 類的時候,必須傳入一個 string 型別的 name 屬性。
接著加一個可選屬性 age:
// 透過?將 age 改成可選。解決:屬性“age”沒有初始化表示式,且未在建構函式中明確賦值。
age?: number
可以設定預設值:
// 根據預設值推斷型別,而且是必選屬性
money = 100
Tip:稍後我們會看到對應的 js 是什麼樣子。
屬性預設是 public
,自身可以用,繼承的子類中也可以使用。public 還可以這麼寫,效果和上例等價:
constructor(name){
- name: string
- constructor(name: string){
+ constructor(public name: string){
this.name =name;
}
另外還有 private
表明只能在類中使用。protected
只能在類和子類中使用。請看示例:
class People{
...
// 屬性預設是 public,自身可以用、繼承也能用
public money2 = 200
private money3 = 300
protected money4 = 400
constructor(name: string){
this.name =name;
}
sayName(){
console.log(this.name)
}
}
let people = new People('aaron')
console.log(people.money);
// 屬性“money3”為私有屬性,只能在類“People”中訪問。ts(2341)
console.log('people.money3: ', people.money3); // 300
// 屬性“money4”受保護,只能在類“People”及其子類中訪問。ts(2445)
console.log('people.money4: ', people.money4); // 400
注
:雖然 vscode 報錯,但瀏覽器控制檯還是輸出了。或許 ts 只是靜態編譯,對應的js 沒有做特殊處理
,比如 private 宣告 money4,實際上並沒有實現。請看ts線上執行環境 ts 對應的 js:
// ts
class People{
name: string
age?: number
money = 100
public money2 = 200
private money3 = 300
protected money4 = 400
constructor(name: string){
this.name =name;
}
sayName(){
console.log(this.name)
}
}
let people = new People('aaron')
console.log('people.money3: ', people.money3);
console.log('people.money4: ', people.money4);
// 對應的js
"use strict";
class People {
constructor(name) {
this.money = 100;
this.money2 = 200;
this.money3 = 300;
this.money4 = 400;
this.name = name;
}
sayName() {
console.log(this.name);
}
}
let people = new People('aaron');
console.log('people.money3: ', people.money3);
console.log('people.money4: ', people.money4);
js 中靜態屬性
使用如下:
protected money4 = 400
// 靜態屬性
+ static flag = 110
console.log('People.flag: ', People.flag);
例如將靜態屬性設定成私有,只能在類中使用。請看示例:
// 靜態屬性
private static flag = 110
// 報錯:屬性“flag”為私有屬性,只能在類“People”中訪問。
console.log('People.flag: ', People.flag);
多個修飾符
可以一起使用,但有時候需要注意順序,vscode 也會給出提示。就像這樣:
// “static”修飾符必須位於“readonly”修飾符之前。ts(1029)
readonly static flag = 110
比如定義一個靜態只讀屬性:
static readonly flag2 = 110
// 報錯:無法為“flag2”賦值,因為它是隻讀屬性。ts(2540)
People.flag2 = 111
類的存取器
感覺就是 js 的 get 和 set。比如下面就是一個 js 的get、set示例:
class People {
constructor(name) {
this.name = name;
}
get name() {
return 'apple';
}
set name(v) {
console.log('set', v);
}
}
let people = new People('aaron') // set aaron
people.name = 'jia' // set jia
console.log(people.name); // apple
對應 ts 中的存取器就是這樣:
class People{
constructor(name: string){
this.name = name;
}
get name(){
return 'apple'
}
set name(v){
console.log('set', v)
}
}
let people = new People('aaron') // set aaron
people.name = 'jia' // set jia
console.log(people.name); // apple
注:這個例子很可能會棧溢位,就像這樣:
class People{
constructor(name: string){
this.name = name;
}
get name(){
return 'apple'
}
set name(v){
console.log('v: ', v);
// 棧溢位
// 報錯:VM47:10 Uncaught RangeError: Maximum call stack size exceeded
this.name = v
}
}
let people = new People('aaron')
所以可以這麼寫:
class People {
private _name: string = ''
get name(): string{
return 'peng'
}
set name(val: string){
this._name = val
}
}
let people = new People()
people.name
// 報錯:屬性“_name”為私有屬性,只能在類“People”中訪問。ts(2341)
people._name
不寫型別,ts 也會自動推導,比如去除型別後也可以。就像這樣:
// 自動推導型別
class People {
private _name = 'peng'
get name(){
return 'peng'
}
set name(val){
this._name = val
}
}
let people = new People()
抽象類
抽象類(abstract),不允許被例項化,抽象屬性和抽象方法必須被子類實現
。更像一個規範。請看示例
abstract class People {
// 可以有抽象屬性和方法
abstract name: string
abstract eat(): void
// 也可以有普通屬性和方法
say() {
console.log('hello: ' + this.name)
}
}
// 如果不實現 name 和 eat 方法則報錯
class Student extends People{
name: string = '學生'
// 既然沒報錯 - 抽象類中返回是 void,這裡返回string
eat(){
return 'eat apple'
}
}
const s1 = new Student()
s1.say()
console.log(s1.eat()); // eat apple
抽象類定義了一個抽象屬性、一個抽象方法,一個具體方法
。子類必須實現抽象屬性和抽象方法,子類例項可以直接訪問抽象類中具體的方法。請看對應的 js 程式碼,你就能很明白。
class People {
say() {
console.log('hello: ' + this.name);
}
}
class Student extends People {
constructor() {
super(...arguments);
this.name = '學生';
}
eat() {
return 'eat apple';
}
}
const s1 = new Student();
s1.say();
console.log(s1.eat());
類實現介面
前面我們用介面定義了一個型別:
interface Person{
name: string,
age: number
}
const p: Person = {
name: 'peng',
age: 18
}
抽象類如果只寫抽象方法和屬性,那麼就和介面很相同了。另外介面用 interface
關鍵字定義,子類可以實現 implements
(注意這個單詞是複數
) 多個介面(不能同時繼承多個)。請看示例:
interface People {
name: string
eat(): void
}
interface A{
age: number
}
// 實現兩個介面,所有屬性和方法都需要實現
class Student implements People, A{
name: string = '學生'
age = 100
// 既然沒報錯
eat(){
return 'eat apple'
}
}
const s1 = new Student()
console.log(s1.eat()); // eat apple
泛型類
使用類時,除了可以使用介面來規範行為,還可以將類和泛型結合,稱為泛型類
。
比如現在 deal 是處理 string 的方法:
class People {
value: string;
constructor(value: string) {
this.value = value;
}
deal(): string {
return this.value;
}
}
const p1 = new People('peng')
p1.deal()
後面我需要 deal 又能處理 number,這樣就可以使用泛型。就像這樣:
class People<T> {
value: T;
constructor(value: T) {
this.value = value;
}
deal(): T {
return this.value;
}
}
const p1 = new People('peng')
p1.deal()
const p2 = new People(18)
p2.deal()
多個泛型寫法如下:
class Pair<T, U> {
private first: T;
private second: U;
constructor(first: T, second: U) {
this.first = first;
this.second = second;
}
public getFirst(): T {
return this.first;
}
public getSecond(): U {
return this.second;
}
}
// 使用帶有多個泛型型別引數的泛型類
let pair1 = new Pair<number, string>(1, "apple");
console.log(pair1.getFirst()); // 1
console.log(pair1.getSecond()); // apple
let pair2 = new Pair<string, boolean>("banana", true);
console.log(pair2.getFirst()); // banana
console.log(pair2.getSecond()); // true
其他
Error Lens
:提供了一種更直觀的方式來展示程式碼中的問題,如錯誤、警告和建議,以幫助開發者更快速地識別和解決問題。
vscode 直接安裝後,會將紅色錯誤提示直接顯示出來,無需將滑鼠移到紅色波浪線才能看到錯誤提示。