TypeScript入门指南

在现代前端和后端开发中,TypeScript 已成为许多开发者的首选工具。作为 JavaScript 的超集,TypeScript 在保留灵活性的同时,通过强大的类型系统提升了代码的可靠性和可维护性。无论是大型项目的复杂需求,还是小型项目的高效开发,TypeScript 都能提供强有力的支持

小贴士
文章中涉及到的代码示例你都可以从 这里查看 ,若对你有用还望点赞支持;你可以在 TS Playground 尝试开始Typescript的学习

本人在工作中看到一些新手同事对Typescript仍很陌生,或者就是能简单使用就行,基本就是cv系列模式,这背后很大程度就是没有对ts有系统的学习和认识。
本指南将带您从零开始,逐步掌握 TypeScript 的核心概念与实际应用,助您开启更加高效和安全的开发旅程

理解类型编程

类型编程是指在 TypeScript 中利用类型系统实现复杂的类型操作和逻辑,类似于在值级别编写程序,但是在类型级别进行操作

换句话来说就是在我们的任何JS代码中通过加上类型注解就有了类型计算系统,任何TS代码删除对应的类型注解都不会影响实际的运行时结果

JS和TS的类型操作二者互不干涉,TS的类型只服务于编译器,用来在coding阶段就告诉开发者有哪些潜在问题。要知道ts是不能在运行时环境直接运行的,所以ts和JS运行时没有半毛钱关系

在编码时二者的coding一般不会有交叉,只有个别几个特殊案例,如:typeofclassin等等,这些看似js和ts中都可以使用,但并不是同一个东西。只要记住真正运行的是js代码,编译后的代码会将所有的类型删掉;ts的类型是个虚拟世界,用来警告和规范开发者的编码

类型编程意义

js因灵活使用而出色,但随着项目不断变大后变量类型、变量名、类型提升、空安全等一系列问题都会让项目难以维护。而ts类型编程就是来通过静态类型检查来降低运行时的错误,同时强大的类型系统也会让开发者更好的理解和约束编码

基础类型

基础类型就是我们在js中常用的一些类型,如:stringnumberboolean。这些类型很好理解,但在ts中时如何表达呢❓

首先要介绍下ts的类型写法:

变量:类型

ts中通常定义某个变量类型(变量、函数等等)都是在其后面加上 :类型 这种形式的,如果读者学过其他语言,如Java那么对于类型编程更容易理解和上手了

来看Java中定义一个函数,它的类型通常都会在前方,变量在后方的形式 类型:变量

Boolean runApp(String appName) {
  int appVersion = new Random().nextInt(10);
  System.out.println("App版本:" + appVersion + " , App名称:" + appName);

  return true;
}

而ts中正好相反,用ts来还原上面的代码:

function runApp(appName: string): boolean {
  const appVersion: number = Math.floor(Math.random() * 10);
  console.log("App版本:" + appVersion + " , App名称:" + appName);

  return true;
}

而在类型世界中对明确的变量值都默认有明确的类型推导,开发者可以省略掉不写

ts通过强类型约束,一旦固定了类型后就无法在赋值成其它类型,上面修改变量值为3,编译器明确提示不能将number赋值给string类型。这就是ts静态编译的好处,如果用js是无法告知这一问题的

重要内置变量

除了js中常见的一些基础变量外,ts中还有一些内置的基础变量,它们对于开发也是很重要的

null/undefined/void

nullundefinedvoid类型和js中的基本一样

配置文件 strictNullChecks: false时,null可以赋值给任何类型,表示当前变量还是空值,后面将会赋值

let appName: string = null;
let appName: string | null = null;

undefined表示当前变量还没有定义

let appVersion: number = undefined;

void可能很多开发者在js中也没有多用过,它其实就是永远不可到达的意思,在js中通常以void 某值的形式使用,void不管后面什么值永远返回undefined

而在ts中也表示没有任何值的意思,通常用作函数的返回使用

any/unknown/never

相比上面的内置类型这几个就比较新颖少见,但anyunknownnever对ts编程非常重要

any表示任意类型,如果将变量定义为此类型,赋值任何类型值都不会报错,等同于放弃了类型检查😂。而且访问任意属性也不会有问题,最大的坏处就是明明可以类型推导的也不好使了

📢 工作中经常会看到很多同事使用any来赋值一个位置变量或者有类型报错问题就赋值成any,这样做会导致一些隐患,让后续维护也变得困难

unknown 表示未知的类型,是一种更安全的动态类型,它和any有什么区别呢❓简单来说就是使用unknown类型的变量时,需要先明确告诉编译器这个变量的类型,而不像any那样随便使用都可以

:::warning 小贴士
尽量不要给变量定义为any类型,对于未知变量要优先使用unknown,采用类型收窄策略处理
:::

never表示不会发生的值或永远不会有返回值的类型。没有值的类型是never类型,它也通常用作类型收窄时的分支终结判断

function useNever(): never {
  // while(true) {}
  throw new Error("error");
}
type TV<U> = U extends string ? string : never;

字面量类型

字面量类型是一种特殊类型,主要是在基础类型的基础上更严格的约束类型,可以说是指定具体的基础类型值作为类型,这个很好理解使用也很广泛

数组/元组

数组可以使用类型[]Array<类型>进行定义

const arr1: number[] = [1, 2, 3];
const arr2: Array<number> = [1, 2, 3];

元组可以理解为特殊的数组,数组定义所有索引的类型,而元组可以定义每个索引的类型[类型, 类型...],并且数组的长度和类型索引长度一致

枚举

枚举用来定义一组常量集合,在没有ts时我们也可以自定义一些常量集合,而使用ts更加方便

enum Color {
  Red,
  Green = 3,
  Blue,
}

常量属性的值可以省略不写,默认以0开始累加,如果有特殊值后面的属性会在此值上新加。除了使用默认的数字值外,也可以使用字符串

上面的枚举会被编译成

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 3] = "Green";
    Color[Color["Blue"] = 4] = "Blue";
})(Color || (Color = {}));

本质还是定义一个对象,然后添加对应的枚举属性并赋值,除此之外也将它们的值作为属性,将属性作为对应的值,正好相反过来

p['Red'] = 0;
p[0] = 'Red';

除此之外enum还有一个常量枚举的概念,二者写法上没有任何差别,唯一不一样的是常量枚举集合不会编译成js对象,只会在ts中有引用的地方直接赋值

const enum Color {
  Red,
  Green = 3,
  Blue,
}
const color: Color = Color.Green;
/// 这段ts编译后如下
var color = 3 /* Color.Green */;

接口interface

interface 是一种抽象的类型描述,用于定义对象、类或函数必须遵守的结构。它提供了静态的类型检查,确保代码符合特定的约定

interface Person {
  name: string;
  age: number;
}
const person: Person = {
  name: 'Tom',
  age: 1,
};

只读

使用 readonly 关键字将属性设置为只读,赋值后无法修改

interface Person {
  readonly age: number;
}
const person: Person = { name: 'Tom', age: 1 };
person.age = 2; // Cannot assign to 'age' because it is a read-only property

可选属性

可以通过在属性名后加上 ? 来定义可选属性,定义时可以不包含这个属性,使用时这个值可能不存在

interface Person {
  sex?: string;
}

索引属性

通过索引签名来约束对象的键和值的类型

interface Person {
  age: number;
  [key: string]: string | number;
}

继承、实现

在面向对象编程语言中最常见的就是继承、实现等用法,在ts中interface和类一样也支持同样的功能

定义一个基类:

interface Car {
  name: string;
  type: "car";
  run(): void;
}

新增一个interface来继承基本的接口:

interface Benz extends Car {
  brand: "Benz";
}

除此之外还可以使用class来实现接口的定义:

class BMW implements Car {
  name: string;
  type: "car";
  brand: "BMW";

  run(): void {
    console.log('BMW is running');
  }
}

接口合并

当定义多个同名接口时,TypeScript 会自动合并它们的成员,这在扩展一些第三方库的时候很有用

interface Person {
  name: string;
}
interface Person {
  age: number;
}
let person: Person;

来看下person的属性是不是都有

类型别名type

类型别名是用来为一个类型(基本类型、对象类型、联合类型、交叉类型等)起一个自定义名称的功能。它可以提升代码的可读性、复用性和灵活性

interface Person { name: string; }
type Student = Person & { grade: number; };
type Color = 'red' | 'green' | 'blue';
type RGB = Color;

虽然 typeinterface 在某些情况下可以互换使用,但在描述联合类型或复杂类型组合时,type 更加灵活

面试中经常会问typeinterface的区别,相信看到这里读者应该明白了

类Class

Class 是对 JS中的class扩展,支持类型检查和一些高级特性,例如访问修饰符、构造函数参数属性、抽象类和接口实现等

除此之外还支持更高级的特殊性,如装饰器,可阅读文章 「Typescript装饰器与注解」

访问修饰符

支持三种访问修饰符,用于控制类的属性和方法的访问范围,publicprotectedprivate,默认是public

class Person {
  name: string;
  protected age: number;
  private sex: string;
}
const person = new Person();

这里就只能访问到name属性,而protectedprivate是控制类的继承可访问范围的

私有构造器

class Person {
  private constructor() {}
}

这里实例化Person就会报错

私有构造器通常结合设计模式,禁止外部直接实例化,而是通过静态属性实现单例或者实例化时做一些其他业务等等。稍微改造下上面的代码:

class Person {
  static instance: Person;
  static getInstance(): Person {
    if (!Person.instance) {
      Person.instance = new Person();
    }
    return Person.instance;
  }
  private constructor() {}
}
const person = Person.getInstance();

继承

集成和接口类似,但是只能集成一个基类,而接口可以继承多个

class Car {
  name: string;
  type: 'Car'
}

class Benz extends Car {
  brand: 'Benz'
}

抽象类

抽象类是不能直接实例化的类,它可以定义抽象方法(没有实现的方法),子类必须实现这些方法

abstract class Car {
  name: string;
  type: 'Car';

  abstract running(): void;
}

class Benz implements Car {
  name: string;
  type: "Car";
  brand: 'Benz'
  running(): void {
    throw new Error("Method not implemented.");
  }
}

子类可以同时实现多个抽象类

class A implements B, C, D {}

断言

断言包括 类型断言非空断言,通常是为了明确告诉编译器这个变量的类型是什么,一般在变量出现多种情况下,如同时存在空值stringnumber等等,那么就可以通过断言的方式明确变量的类型

可以使用 as<类型>!等几种方式进行断言

let x: string | number;
x! * 2;
(x! as number) * 2;
<number>x! * 2;
let obj: { x?: number } = {};
obj.x += 1;
obj.x! * 2;

对应的编译报错情况如下:

除此之外 as 还支持 双重断言,一般是为了解决复杂的类型判断,先断言为 any 再断言为具体的类型,这样来绕过复杂的类型检查

data as any as Person

泛型

泛型用于创建可重用、灵活且具有类型安全的代码。泛型允许在定义函数、类或接口时,不指定具体的类型,而是通过参数化的方式将类型作为变量传递

几乎所有的编程语言都支持泛型特性,为什么需要泛型❓

在开发过程中,函数或类可能需要处理不同类型的数据。如果为每种类型分别定义代码,会导致冗余;如果使用 any 类型,则丧失了类型安全。泛型可以在代码复用与类型安全之间取得平衡

:::warning 小插曲
工作中经常会看到新手对泛型有一定的抵触,觉得泛型不好理解,其实很简单!开发者把它当成平时定义的变量来使用就行,只不过它用来出来类型的,而不是真实变量,新手小伙伴一定不能慌
:::

常见的泛型使用

这里列举一些常见的泛型使用方式

function functionName<T>(parameter: T): T { return parameter; }
const api = <T>() => Promise<T>;
class Person<T> {
  name: T;
}
interface Pair<K, V> {
  key: K;
  value: V;
}
type Test<T> = T extends number ? string : number;

泛型约束

有时需要限制泛型的范围,使其只能使用某些特定类型>。可以通过 extends 关键字为泛型添加约束

interface HasLength {
  length: number;
}
function logLength<T extends HasLength>(value: T): void {
  console.log(value.length);
}
function logStr<T extends string>(value: T): void {
  console.log(value);
}

索引类型keyof

keyof 用于获取对象类型的所有键(属性名)组成的联合类型。它通常与泛型、映射类型等结合使用,以增强类型安全性和灵活性

type Person = {
  name: string;
  age: number;
  location: string;
};
type PersonKeys = keyof Person; // "name" | "age" | "location"

类型查询typeof

typeof通常用于根据某个对象来获取对应的类型,可以帮助我们动态地推断类型,尤其是在处理复杂类型或需要复用类型时非常有用

const person = {
  name: 'Tom',
  age: 1
}
type Person = typeof person;

// 等价于
type Person = {
  name: string;
  age: number
}

类型映射

类型映射可以基于已有类型生成新的类型。这种特性通过遍历对象类型的键并对其应用变换规则来实现,通常用于对类型进行批量修改或创建灵活的类型

类型映射的核心是 inkeyof 关键字,它允许对类型的每个属性进行迭代和处理

type Person = {
  name: string;
  age: number;
};

// 将所有属性设置为只读
type ReadonlyPerson = {
  readonly [Key in keyof Person]: Person[Key];
};

keyof列出所有的属性后,通过in来遍历所有的属性,然后可以对属性做相关调整

上面对每个属性加了readonly只读属性,也可以在访问属性时通过as修改属性名:

type ReadonlyPerson = {
  readonly [Key in keyof Person as `readonly_${Key}`]: Person[Key];
};

这里发现每个属性都加了readonly前缀

除此之外你还可以通过条件判断订制更复杂的规则

联合类型和交叉类型

联合类型(Union Types) 和 交叉类型(Intersection Types) 是两种非常重要的类型操作符,用于组合多个类型。它们分别用于表示“或”和“且”的关系

联合类型表示一个值可以是多种类型中的一种。它使用 | 符号将多个类型组合在一起

type StringOrNumber = string | number;

let value: StringOrNumber;
value = "hello"; // 合法
value = 42;      // 合法
value = true;

联合类型值的类型可以是联合类型中的任意一种,而且只能访问所有类型共有的成员(除非使用类型守卫)

交叉类型表示一个值必须同时满足多个类型的条件。它使用 & 符号将多个类型组合在一起

type Person = {
  name: string;
  age: number;
};

type Employee = {
  id: number;
  department: string;
};

type EmployeePerson = Person & Employee;

const employee: EmployeePerson = {
  name: "Alice",
  age: 30,
  id: 123,
  department: "Engineering",
};

交叉类型值的类型必须同时满足所有类型的条件,通常用于合并多个类型的属性

:::warning 小贴士
本篇文章不会太抠一些不常见的类型计算或者专门搞一些偏门的类型体操问题,个人觉得没有太大意义,也不需要去记什么特殊情况,通常只要大家对ts熟练使用后,对于实际问题亲自上手都是可以搞定的
:::

条件类型

条件类型(Conditional Types)允许我们根据类型关系动态地选择类型。条件类型的语法类似于三元表达式,可以根据条件从两个可能的类型中选择一个

T extends U ? X : Y

读者可以直接把它当做JS中的条件三元运算,extends你可以认为是instanceof,也就是左侧的类型可以为右侧的子类型或者就是右侧类型

条件类型通常与泛型结合使用,用于创建更灵活和可复用的类型

来看个🌰

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

和JS一样类型也可以无限判断下去:

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : "unknown";

type A = TypeName<"hello">; // "string"
type B = TypeName<42>;      // "number"
type C = TypeName<true>;    // "boolean"
type D = TypeName<{}>;      // "unknown"

infer

infer 是条件类型中的一个关键字,用于在条件类型中推断类型。它通常用于提取复杂类型的某一部分

提取函数返回类型:

type FnReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = FnReturnType<() => string>; // string
type B = FnReturnType<() => number>; // number

infer R 表示推断函数的返回类型,并将其赋值给 R

提取数组元素类型:

type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number

可以看到infer可以用的位置有很多,就不一一介绍了,新手一定要多尝试写才会加深印象

分布式条件类型

当条件类型作用于联合类型时,它会自动分发到联合类型的每个成员上,这种行为称为分布式条件类型

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>; // string[] | number[]

如果不想要这种默认的分发,可以将extends两侧的条件都加上[]

type ToArray<T> = [T] extends [any] ? T[] : never;

type A = ToArray<string | number>; // (string | number)[]

一般我们会将其总结为:裸露条件会分发,加上[]后就像穿上衣服一样,不会进行分发了

递归条件类型

条件类型可以递归使用,用于处理嵌套的类型结构

type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;

type A = Flatten<string[][]>; // string
type B = Flatten<number[][][]>; // number

这里在获取对应类型时都会先判断当前类型是不是类型[]这样的,如果是证明还是个复合类型,那么就可以通过infer[]左侧的类型提取出,通过递归再次进行计算,这样层层递归直到满足右侧条件

类型守卫

类型守卫(Type Guards)用于在代码中缩小变量的类型范围。通过类型守卫,我们可以在特定的代码块中明确知道一个变量的具体类型,从而安全地访问该类型的属性和方法

通常实际工作中的接口类型都不是简单的一种类型,可能会根据某个属性不断变化,这时候在处理业务时需要针对不同的类型区别处理,而在ts类型层面也要将明确的类型给到编译器

为满足不同场景使用,Typescript提供了多种方式来实现类型守卫,包括typeofinstanceofin等等方式,我们来一一尝试一下

typeof

typeof 是JS中的原生操作符,用于检查值的类型,ts也可以根据它推导出类型信息

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // 这里 value 是 string 类型
  } else {
    console.log(value.toFixed(2)); // 这里 value 是 number 类型
  }
}

instanceof

instanceof 也是JS原生操作符,可以用于检查一个对象是否是某个类的实例来缩小类型范围

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // 这里 animal 是 Dog 类型
  } else {
    animal.meow(); // 这里 animal 是 Cat 类型
  }
}

in

in 也是原生JS操作符,也可以用于检查某个属性是否存在,从而推断出变量的具体类型

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat) {
  if ("bark" in animal) {
    animal.bark(); // 这里 animal 是 Dog 类型,Dog类型有bark方法
  } else {
    animal.meow(); // 这里 animal 是 Cat 类型
  }
}

as/<类型>

as断言操作符用于显式地告诉编译器某个值的具体类型。它通常用于在 TypeScript 无法自动推断类型的情况下,手动指定类型

declare const param: string | number;
(param as string).toUpperCase();
(<number>param).toFixed();

读者可以发现这些都是前面学过的知识,在这里直接结合用就ok

is

除此之外我们也可以通过编写自定义函数来实现类型守卫。自定义类型守卫函数的返回值是一个类型谓词

function isWhatType(parameter): parameter is WhatType {}

这种形式的判断高度自由,结果只需要加上对应的类型就可以

interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // 这里 pet 是 Fish 类型
  } else {
    pet.fly(); // 这里 pet 是 Bird 类型
  }
}

工作中总是都会遇到很多复杂的场景,通常都需要将上面的多种形式结合使用

内置工具

TypeScript 提供了许多内置的工具类型(Utility Types),这些工具类型可以帮助我们更方便地操作和转换类型

// 1. Partial<T>:将类型 T 的所有属性变为可选
interface User {
  name: string;
  age: number;
}
type PartialUser = Partial<User>; // { name?: string; age?: number }

// 2. Required<T>:将类型 T 的所有属性变为必填
type RequiredUser = Required<PartialUser>; // { name: string; age: number }

// 3. Readonly<T>:将类型 T 的所有属性变为只读
type ReadonlyUser = Readonly<User>; // { readonly name: string; readonly age: number }

// 4. Record<K, T>:创建一个对象类型,其键为 K,值为 T
type UserRoles = Record<string, string>; // { [key: string]: string }

// 5. Pick<T, K>:从类型 T 中选取指定的属性 K
type UserNameAndAge = Pick<User, "name" | "age">; // { name: string; age: number }

// 6. Omit<T, K>:从类型 T 中排除指定的属性 K
type UserWithoutAge = Omit<User, "age">; // { name: string }

// 7. Exclude<T, U>:从类型 T 中排除可以赋值给 U 的类型
type T = string | number | boolean;
type StringOrNumber = Exclude<T, boolean>; // string | number

// 8. Extract<T, U>:从类型 T 中提取可以赋值给 U 的类型
type NumberOrBoolean = Extract<T, number | boolean>; // number | boolean

// 9. NonNullable<T>:从类型 T 中排除 null 和 undefined
type NonNullableT = NonNullable<string | number | null | undefined>; // string | number

// 10. ReturnType<T>:获取函数类型 T 的返回值类型
function getUser() {
  return { name: "Alice", age: 30 };
}
type UserReturnType = ReturnType<typeof getUser>; // { name: string; age: number }

// 11. Parameters<T>:获取函数类型 T 的参数类型组成的元组
function add(a: number, b: number) {
  return a + b;
}
type AddParams = Parameters<typeof add>; // [number, number]

// 12. ConstructorParameters<T>:获取构造函数类型 T 的参数类型组成的元组
class Person {
  constructor(public name: string, public age: number) {}
}
type PersonParams = ConstructorParameters<typeof Person>; // [string, number]

// 13. InstanceType<T>:获取构造函数类型 T 的实例类型
type PersonInstance = InstanceType<typeof Person>; // Person

// 14. ThisParameterType<T>:获取函数类型 T 的 this 参数类型
function greet(this: { name: string }) {
  console.log(`Hello, ${this.name}`);
}
type GreetThis = ThisParameterType<typeof greet>; // { name: string }

// 15. OmitThisParameter<T>:从函数类型 T 中移除 this 参数
type GreetWithoutThis = OmitThisParameter<typeof greet>; // () => void

// 16. Awaited<T>:获取 Promise 类型 T 的解析值类型
type PromiseResult = Awaited<Promise<string>>; // string

// 17. 字符串操作工具类型
type Greeting = "hello";
type UppercaseGreeting = Uppercase<Greeting>; // "HELLO"
type LowercaseGreeting = Lowercase<UppercaseGreeting>; // "hello"
type CapitalizedGreeting = Capitalize<Greeting>; // "Hello"
type UncapitalizedGreeting = Uncapitalize<CapitalizedGreeting>; // "hello"

总结

到这里我们将Typescript基本的类型使用规则就介绍完了,当然ts的使用还远不如这些,但这对于新手来说是首先要了解的整体类型工具体系

本篇教程让你学会了解和学会了如何类型编程,而如何控制ts的行为、以及如何结合第三方工具一起使用就需要读者进一步学习了。接下来我们来学习 Typescript配置文件与运行 吧!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注