2

前端 Typescript 入门 - 彭加李

 1 month ago
source link: https://www.cnblogs.com/pengjiali/p/18104072
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前端 Typescript 入门

Ant design vue4.x 基于 vue3,示例默认是 TypeScript。比如 table 组件管理。

vue3 官网介绍也使用了 TypeScript,例如:响应式 API:核心

华为的鸿蒙OS(HarmonyOS)开发中也可以使用 TypeScript

本篇目的用于对 TS 进行扫盲

Tipts 路线图

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')

通常用于函数,表示没有 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 直接安装后,会将红色错误提示直接显示出来,无需将鼠标移到红色波浪线才能看到错误提示。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK