深入浅出TypeScript | 青训营

139 阅读6分钟

深入浅出TypeScript

为什么

🥰 ts 的使用度逐年上升,ts 社区的活跃度也在明显上升。

TS 是 JS 的超集。

TS 相对于 JS 有哪些优势 🧐:

  • 用于解决大型项目的代码复杂性
  • 强类型,支持静态和动态类型(动态弱类型语言)
  • 可以在编译期间发现并纠正错误(JS 只能在运行时发现错误)
  • 不允许改变变量的数据类型(JS 变量可以被赋予成不同类型)

TS 不仅是一门语言,更是生产力工具。它提供了相对完善的类型检查,同时也带来了一套完整的工具链,可以结合 IDE 的使用来提高代码的质量。

推荐教程:

TS 基础

基础类型

  • boolean, number, string

  • ✨ 枚举 enum

    使用 enum 可以让我们更清晰地表达代码的意图,提高代码的可读性。

    enum Response {
      No = 0,
      Yes = 1,
    }
    ​
    function respond(recipient: string, message: Response) {
      // ...
    }
    ​
    respond('Alice', Response.Yes);
    
  • ✨ any, unkown, void

  • ✨ never

    我们需要 never 类型,因为它可以帮助我们更好地描述代码的行为。例如,在上面的例子中,使用 never 类型可以清楚地表明 raiseError 函数永远不会返回。此外,由于 never 类型是所有类型的子类型,它还可以用于类型检查和类型推断。例如,在使用类型保护缩小变量类型时,如果某个分支永远不会发生,那么该分支的类型就是 never 类型。这样可以帮助我们更好地理解代码,并避免潜在的错误。

    function raiseError(message: string): never {
        throw new Error(message);
    }
    
  • array

  • tuple

函数类型

TS 定义函数类型时要定义输入参数类型和输出类型。

  • 输入参数:支持可选参数和默认参数

  • 输出参数:可以自动推断,没有返回值时默认为 void

  • 函数重载:名称相同参数不同,可以通过重载支持多种类型

    在 TypeScript 中,🪧 函数重载是指在同一个作用域内定义多个同名函数,但它们的参数类型和数量不同。当调用这个函数时,TypeScript 会根据传入的参数类型和数量来选择正确的函数实现。通过使用函数重载,我们可以更好地描述函数的行为,并提供更好的类型检查和代码提示。

    // 函数重载使用案例
    interface User {
        id: number;
        name: string;
    }
    ​
    function getUser(id: number): User;
    function getUser(name: string): User;
    function getUser(param: number | string): User {
        if (typeof param === 'number') {
            // 根据用户 ID 获取用户信息
            return { id: param, name: 'Alice' };
        } else {
            // 根据用户名获取用户信息
            return { id: 1, name: param };
        }
    }
    ​
    const user1 = getUser(1); // 根据用户 ID 获取用户信息
    const user2 = getUser('Bob'); // 根据用户名获取用户信息

interface

🎯 接口是为了定义对象类型。

  • 可选属性 ?

    interface User {
        id: number;
        name: string;
        age?: number; // 可选属性
    }
    
  • 只读属性 readonly

    interface User {
        readonly id: number; // 只读属性
        name: string;
    }
    
  • 可以描述函数类型

    // 一个描述函数类型的接口,接受两个 string 类型的参数,并返回一个 boolean 类型的值
    interface SearchFunc {
        (source: string, subString: string): boolean;
    }
    ​
    let mySearch: SearchFunc;
    mySearch = function(source: string, subString: string) {
        let result = source.search(subString);
        return result > -1;
    }
    
  • 可以描述自定义属性

    interface StringArray {
        [index: number]: string;
    }
    

🎯 写法基本与 JS 一致,增加了一些定义。

特点:

  • 增加了 public. private, protected 修饰符

  • 抽象类:

    • 只能被继承,不能被实例化
    • 作为基类,抽象方法必须被子类所实现
  • interface 约束类,使用 implements 关键字

TypeScript 高级进阶

  • 联合类型 |

  • 交叉类型 &

    interface A {
        a: string;
    }
    ​
    interface B {
        b: number;
    }
    ​
    let x: A & B;
    x = { a: 'hello', b: 42 }; // OK
    x = { a: 'hello' }; // Error: Property 'b' is missing in type '{ a: string; }' but required in type 'B'
    
  • 类型断言

    let x: any = 'hello';
    let y = (<string>x).length; // OK
    let z = (x as string).length; // OK
    
  • 类型别名(type or interface)

    • 给类型起别名
    • 涉及类时使用 interface,不涉及类时一般都是 type
    type MyString = string;
    let x: MyString = 'hello'; // OK
    

泛型

⌛️ 组件要支持当前的数据类型,也要能够支持未来的数据类型。

基本定义 💡

  1. 泛型的语法是 <> 里写类型参数,一般用 T 来表示

  2. 使用时有两种方法指定类型

    1. 定义要使用的类型
    2. 通过 TS 类型推断,自动推导类型
  3. 泛型的作用是临时占位,之后通过传来的类型进行推导

function print<T>(arg:T):T {
    console.log(arg);
    return arg;
}
​
// 指定类型
print<string>('hello!');
// 自动推导类型
print('hello!');

基础操作符 🔨

  • typeof:获取类型

    // 设置 Sam 的类型与 sam 的类型相同
    type Sam = typeof sam;
    
  • keyof: 获取所有键

    // 获取 Person 接口中所有数据类型
    type H1 = keyof Person;
    // 获取 Person 接口中所有数据
    type H2 = keyof Person[];
    
  • in:便利枚举

    type Keys "a" | "b" | "c"type Obj = {
        [p in Keys]: any
    } // {a: any, b: any, c: any}
    
  • T[K]:索引访问

    interface IPerson {
        name: string;
    }
    ​
    let type1: IPerson['name'] //string
    
  • extends:泛型约束

    我们定义了一个名为 HasLength 的接口,它包含一个 length 属性。然后,我们定义了一个名为 getLength 的泛型函数,并使用泛型约束来限制类型参数 T 必须扩展自 HasLength 接口。这样,在调用该函数时,我们只能传递包含 lengthd 属性的对象。

    interface HasLength {
        length: number;
    }
    ​
    function getLength<T extends HasLength>(obj: T): number {
        return obj.length;
    }
    ​
    // 使用泛型约束
    const len1 = getLength([1, 2, 3]); // 类型为 number
    const len2 = getLength('abc'); // 类型为 number
    const len3 = getLength({ length: 10 }); // 类型为 number
    

常用工具类型

  • Partial:将类型属性变为可选
  • Required:将类型属性变为必选
  • ReadOnly:将类型属性变为只读
  • Pick, Record ...

TypeScript 实战

声明文件

  • declare:三方库需要类型声明文件

    declare var $: any;
    
  • .d.ts:声明文件定义

    // jquery.d.ts
    declare var $: any;
    
  • @types:三方库 TS 类型包
  • tsconfig.json:定义 TS 的配置

案例 - ⚗️泛型约束后端接口类型

使用泛型约束来定义后端接口类型的好处 💪 在于,它可以帮助您在编写代码时捕获类型错误。

例如,在下面的例子中,如果尝试访问响应数据中不存在的属性,TypeScript 将会在编译时报错。

// 定义 User 接口,表示用户信息
interface User {
    id: number;
    name: string;
    email: string;
}
​
// 定义 ApiResponse 泛型接口,表示后端接口的响应数据
interface ApiResponse<T> {
    data: T;
    error?: string;
}
​
type GetUserResponse = ApiResponse<User>;
​
async function getUser(id: number): Promise<GetUserResponse> {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    return data;
}
​

我们定义了一个类型别名 GetUserResponse,它表示获取用户信息接口的响应类型。该类型别名使用了泛型约束,将 ApiResponse 接口的类型参数 T 指定为 User 类型。

之后,我们定义了一个名为 getUser 的异步函数,用于从后端获取用户信息。该函数的返回类型为 Promise,表示它返回一个解析为 GetUserResponse 类型的 Promise 对象。

补充

避免沦为 AnyScript

  • 重构时,为了减小工作成本可以适当使用一些 any
  • 新项目中,尽量提前做好类型定义,避免过多使用 any