你不知道的 TypeScript 高级类型(上)

简介: 你不知道的 TypeScript 高级类型(上)

大家好,我是 CUGGZ。

在开发过程中,为了应对多变的复杂场景,我们需要了解一下 TypeScript 的高级类型。所谓高级类型,是 TypeScript 为了保证语言的灵活性,所使用的一些语言特性。这些特性有助于我们应对复杂多变的开发场景。

本文大纲如下:

  1. 字面量类型
  2. 联合类型
  3. 交叉类型
  4. 索引类型
  5. 条件类型
  6. 类型推断
  7. 类型保护
  8. 类型断言


1. 字面量类型


在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即字面量类型。TypeScript 支持以下字面量类型:

  • 字符串字面量类型;
  • 数字字面量类型;
  • 布尔字面量类型;
  • 模板字面量类型。

(1)字符串字面量类型

字符串字面量类型其实就是字符串常量,与字符串类型不同的是它是具体的值:

type Name = "TS";
const name1: Name = "test"; // ❌ 不能将类型“"test"”分配给类型“"TS"”。ts(2322)
const name2: Name = "TS";

实际上,定义单个字面量类型在实际应用中并没有太大的用处。它的应用场景就是将多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合:

type Direction = "north" | "east" | "south" | "west";
function getDirectionFirstLetter(direction: Direction) {
  return direction.substr(0, 1);
}
getDirectionFirstLetter("test"); // ❌ 类型“"test"”的参数不能赋给类型“Direction”的参数。
getDirectionFirstLetter("east");

这个例子中使用四个字符串字面量类型组成了一个联合类型。这样在调用函数时,编译器就会检查传入的参数是否是指定的字面量类型集合中的成员。通过这种方式,可以将函数的参数限定为更具体的类型。这不仅提升了代码的可读性,还保证了函数的参数类型。

除此之外,使用字面量类型还可以为我们提供智能的代码提示:

666.webp.jpg

(2)数字字面量类型

数字字面量类型就是指定类型为具体的数值:

type Age = 18;
interface Info {
  name: string;
  age: Age;
}
const info: Info = {
  name: "TS",
  age: 28 // ❌ 不能将类型“28”分配给类型“18”
};

可以将数字字面量类型分配给一个数字,但反之是不行的:

let val1: 10|11|12|13|14|15 = 10;
let val2 = 10;
val2 = val1;
val1 = val2; // ❌ 不能将类型“number”分配给类型“10 | 11 | 12 | 13 | 14 | 15”。

(3)布尔字面量类型

布尔字面量类型就是指定类型为具体的布尔值(truefalse):

let success: true;
let fail: false;
let value: true | false;
success = true;
success = false;  // ❌ 不能将类型“false”分配给类型“true”

由于布尔字面量类型只有truefalse两种,所以下面 value 变量的类型是一样的:

letvalue: true | false;
letvalue: boolean;

(4)模板字面量类型

在 TypeScript 4.1 版本中新增了模板字面量类型。什么是模板字面量类型呢?它一字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。它与 JavaScript 的模板字符串语法相同,但是只能用在类型定义中使用。

① 基本语法

当使用模板字面量类型时,它会替换模板中的变量,返回一个新的字符串字面量。

type attrs = "Phone" | "Name";
type target = `get${attrs}`;
// type target = "getPhone" | "getName";

可以看到,模板字面量类型的语法简单,并且易读且功能强大。

假如有一个CSS内边距规则的类型,定义如下:

typeCssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom';

上面的类型是没有问题的,但是有点冗长。marginpadding 的规则相同,但是这样写我们无法重用任何内容,最终就会得到很多重复的代码。

下面来使用模版字面量类型来解决上面的问题:

type Direction = 'left' | 'right' | 'top' | 'bottom';
type CssPadding = `padding-${Direction}`
// type CssPadding = 'padding-left' | 'padding-right' | 'padding-top' | 'padding-bottom'

这样代码就变得更加简洁。如果想创建margin类型,就可以重用Direction类型:

typeCssMargin = `margin-${Direction}`

如果在 JavaScript 中定义了变量,就可以使用 typeof 运算符来提取它:

const direction = 'left';
type CssPadding = `padding-${typeof direction}`;
// type CssPadding = "padding-left"

② 变量限制

模版字面量中的变量可以是任意的类型吗?可以使用对象或自定义类型吗?来看下面的例子:

type CustomObject = {
  foo: string
}
type target = `get${CustomObject}`
// ❌ 不能将类型“CustomObject”分配给类型“string | number | bigint | boolean | null | undefined”。
type complexUnion = string | number | bigint | boolean | null | undefined;
type target2 = `get${complexUnion}`  // ✅

可以看到,当在模板字面量类型中使用对象类型时,就报错了,因为编译器不知道如何将它序列化为字符串。实际上,模板字面量类型中的变量只允许是stringnumberbigintbooleannullundefined或这些类型的联合类型。

③ 实用程序

Typescript 提供了一组实用程序来帮助处理字符串。它们不是模板字面量类型独有的,但与它们结合使用时很方便。完整列表如下:

  • Uppercase:将类型转换为大写;
  • Lowercase:将类型转换为小写;
  • Capitalize:将类型第一个字母大写;
  • Uncapitalize:将类型第一个字母小写。

这些实用程序只接受一个字符串字面量类型作为参数,否则就会在编译时抛出错误:

type nameProperty = Uncapitalize<'Name'>;
// type nameProperty = 'name';
type upercaseDigit = Uppercase<10>;
// ❌ 类型“number”不满足约束“string”。
type property = 'phone';
type UppercaseProperty = Uppercase<property>;
// type UppercaseProperty = 'Property';

下面来看一个更复杂的场景,将字符串字面量类型与这些实用程序结合使用。将两种类型进行组合,并将第二种类型的首字母大小,这样组合之后的类型符合驼峰命名法:

type actions = 'add' | 'remove';
type property = 'name' | 'phone';
type result = `${actions}${Capitalize<property>}`;
// type result = addName | addPhone | removeName | removePhone

④ 类型推断

在上面的例子中,我们使用使用模版字面量类型将现有的类型组合成新类型。下面来看看如何使用模板字面量类型从组合的类型中提取类型。这里就需要用到infer关键字,它允许我们从条件类型中的另一个类型推断出一个类型。

下面来尝试提取字符串字面量 marginRight 的根节点:

type Direction = 'left' | 'right' | 'top' | 'bottom';
type InferRoot<T> = T extends `${infer K}${Capitalize<Direction>}` ? K : T;
type Result1 = InferRoot<'marginRight'>;
// type Result1 = 'margin';
type Result2 = InferRoot<'paddingLeft'>;
// type Result2 = 'padding';

可以看到, 模版字面量还是很强大的,不仅可以创建类型,还可以解构它们。

⑤ 作为判别式

在TypeScript 4.5 版本中,支持了将**模板字面量串类型作为判别式,**用于类型推导。来看下面的例子:

interface Message {
    type: string;
    url: string;
}
interface SuccessMessage extends Message {
    type: `${string}Success`;
    body: string;
}
interface ErrorMessage extends Message {
    type: `${string}Error`;
    message: string;
}
function handler(r: SuccessMessage | ErrorMessage) {
    if (r.type === "HttpSuccess") { 
        let token = r.body;
    }
}

在这个例子中,handler 函数中的 r 的类型就被推断为 SuccessMessage。因为根据 SuccessMessageErrorMessage 类型中的type字段的模板字面量类型推断出 HttpSucces 是根据SuccessMessage中的type创建的。


2. 联合类型


(1)基本使用

联合类型是一种互斥的类型,该类型同时表示所有可能的类型。联合类型可以理解为多个类型的并集。 联合类型用来表示变量、参数的类型不是某个单一的类型,而可能是多种不同的类型的组合,它通过 | 来分隔不同的类型:

typeUnion = "A" | "B" | "C";

在编写一个函数时,该函数的期望参数是数字或者字符串,并根据传递的参数类型来执行不同的逻辑。这时就用到了联合类型:

function direction(param: string | number) {
  if (typeof param === "string") {
    ...
  }
  if (typeof param === "number") {
    ...
  }
  ...
}

这样在调用 direction 函数时,就可以传入stringnumber类型的参数。当联合类型比较长或者想要复用这个联合类型的时候,就可以使用类型别名来定义联合类型:

typescript

复制代码

typeParams = string | number | boolean;

再来看一个字符串字面量联合类型的例子,setStatus 函数只能接受某些特定的字符串值,就可以将这些字符串字面量组合成一个联合类型:


type Status = 'not_started' | 'progress' | 'completed' | 'failed';
const setStatus = (status: Status) => {
  db.object.setStatus(status);
};
setStatus('progress');
setStatus('offline'); // ❌ 类型“"offline"”的参数不能赋给类型“Status”的参数。

在调用函数时,如果传入的参数不是联合类型中的值,就会报错。

(2)限制

联合类型仅在编译时是可用的,这意味着我们不能遍历这些值。进行如下尝试:

typeStatus = 'not_started' | 'progress' | 'completed' | 'failed';

console.log(Object.values(Status)); // ❌ “Status”仅表示类型,但在此处却作为值使用。

这时就会抛出一个错误,告诉我们不能将 Status 类型当做值来使用。

如果想要遍历这些值,可以使用枚举来实现:

enum Status {
  'not_started',
  'progress',
  'completed',
  'failed'
}
console.log(Object.values(Status));

(3)可辨识联合类型

在使用联合类型时,如何来区分联合类型中的类型呢?类型保护是一种条件检查,可以帮助我们区分类型。在这种情况下,类型保护可以让我们准确地确定联合中的类型(下文会详细介绍类型保护)。

有很多方式可以做到这一点,这很大程度上取决于联合类型中包含哪些类型。有一条捷径可以使联合类型中的类型之间的区分变得容易,那就是可辨识联合类型。可辨识联合类型是联合类型的一种特殊情况,它允许我们轻松的区分其中的类型。

这是通过向具有唯一值的每个类型中添加一个字段来实现的,该字段用于使用相等类型保护来区分类型。例如,有一个表示所有可能的形状的联合类型,根据传入的不同类型的形状来计算该形状的面积,代码如下:


type Square = {
  kind: "square";
  size: number;
}
type Rectangle = {
  kind: "rectangle";
  height: number;
  width: number;
}
type Circle = {
  kind: "circle";
  radius: number;
}
type Shape = Square | Rectangle | Circle; 
function getArea(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
  }
}

在这个例子中,Shape 就是一个可辨识联合类型,它是三个类型的联合,而这三个类型都有一个 kind 属性,且每个类型的 kind 属性值都不相同,能够起到标识作用。 函数内应该包含联合类型中每一个接口的 case以保证每个**case**都能被处理。

如果函数内没有包含联合类型中每一个类型的 case,在编写代码时希望编译器应该给出代码提示,可以使用以下两种完整性检查的方法。

① strictNullChecks

对于上面的例子,先来新增一个类型,整体代码如下:

type Square = {
  kind: "square";
  size: number;
}
type Rectangle = {
  kind: "rectangle";
  height: number;
  width: number;
}
type Circle = {
  kind: "circle";
  radius: number;
}
type Triangle = {
  kind: "triangle";
  bottom: number;
  height: number;
}
type Shape = Square | Rectangle | Circle | Triangle; 
function getArea(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
  }
}

这时,Shape 联合类型中有四种类型,但函数的 switch 里只包含三个 case,这个时候编译器并没有提示任何错误,因为当传入函数的是类型是 Triangle 时,没有任何一个 case 符合,则不会执行任何 return 语句,那么函数是默认返回 undefined。所以可以利用这个特点,结合 strictNullChecks 编译选项,可以在tsconfig.json配置文件中开启 strictNullChecks

{
  "compilerOptions": {
    "strictNullChecks": true,
  }
}

让函数的返回值类型为 number,那么当返回 undefined 时就会报错:

function getArea(s: Shape): number {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
  }
}

上面的number处就会报错:888.webp.jpg

② never

当函数返回一个错误或者不可能有返回值的时候,返回值类型为 never。所以可以给 switch 添加一个 default 流程,当前面的 case 都不符合的时候,会执行 default 中的逻辑:

function assertNever(value: never): never {
  throw new Error("Unexpected object: " + value);
}
function getArea(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
    default:
      return assertNever(s); // error 类型“Triangle”的参数不能赋给类型“never”的参数
  }
}

采用这种方式,需要定义一个额外的 asserNever 函数,但是这种方式不仅能够在编译阶段提示遗漏了判断条件,而且在运行时也会报错。


3. 交叉类型


(1)基本实用

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到成为一种类型,合并后的类型将拥有所有成员类型的特性。交叉类型可以理解为多个类型的交集。 可以使用以下语法来创建交叉类型,每种类型之间使用 & 来分隔:

typeTypes = type1 & type2 & .. & .. & typeN;

如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何意义的,因为不会有变量同时满足这些类型,那这个类型实际上就等于never类型。

(2)使用场景

上面说了,一般情况下使用交叉类型是没有意义的,那什么时候该使用交叉类型呢?下面就来看看交叉类型的使用场景。

① 合并接口类型

交叉类型的一个常见的使用场景就是将多个接口合并成为一个:

type Person = {
  name: string;
  age: number;
} & {
  height: number;
  weight: number;
} & {
  id: number;
}
const person: Person = {
  name: "zhangsan",
  age: 18,
  height: 180,
  weight: 60,
  id: 123456
}

这里就通过交叉类型使 Person 同时拥有了三个接口中的五个属性。那如果两个接口中的同一个属性定义了不同的类型会发生了什么情况呢?

type Person = {
  name: string;
  age: number;
} & {
  age: string;
  height: number;
  weight: number;
}

两个接口中都拥有age属性,并且类型分别是numberstring,那么在合并后,age的类型就是string & number,也就是 never 类型:

const person: Person = {
  name: "zhangsan",
  age: 18,   // ❌ 不能将类型“number”分配给类型“never”。
  height: 180,
  weight: 60,
}

如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型——数字字面量类型,合并后 age 属性的类型就是两者中的子类型:

type Person = {
  name: string;
  age: number;
} & {
  age: 18;
  height: number;
  weight: number;
}
const person: Person = {
  name: "zhangsan",
  age: 20,  // ❌ 不能将类型“20”分配给类型“18”。
  height: 180,
  weight: 60,
}

第二个接口中的age是一个数字字面量类型,它是number类型的子类型,所以合并之后的类型为字面量类型18

② 合并联合类型

交叉类型另外一个常见的使用场景就是合并联合类型。可以将多个联合类型合并为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员:

type A = "blue" | "red" | 999;
type B = 999 | 666;
type C = A & B; // type C = 999;constc: C = 999;

如果多个联合类型中没有相同的类型成员,那么交叉出来的类型就是never类型:

type A = "blue" | "red";
type B = 999 | 666;
type C = A & B;
const c: C = 999; // ❌ 不能将类型“number”分配给类型“never”。


4. 索引类型


在介绍索引类型之前,先来了解两个类型操作符:索引类型查询操作符索引访问操作符

(1)索引类型查询操作符

使用 keyof 操作符可以返回一个由这个类型的所有属性名组成的联合类型:

type UserRole = 'admin' | 'moderator' | 'author';
type User = {
  id: number;
  name: string;
  email: string;
  role: UserRole;
}
type UserKeysType = keyof User; // 'id' | 'name' | 'email' | 'role';

(2)索引访问操作符

索引访问操作符就是[],其实和访问对象的某个属性值是一样的语法,但是在 TS 中它可以用来访问某个属性的类型:

type User = {
  id: number;
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
}
type Params = {
  id: User['id'],
  address: User['address']
}

这里我们没有使用number来描述id属性,而是使用 User['id'] 引用User中的id属性的类型,这种类型成为索引类型,它们看起来与访问对象的属性相同,但访问的是类型。

当然,也可以访问嵌套属性的类型:

typeCity = User['address']['city']; // string

可以通过联合类型来一次获取多个属性的类型:

typeIdOrName = User['id' | 'name']; // string | number

(3)应用

我们可以使用以下方式来获取给定对象中的任何属性:


function getPropertyextends keyof T>(obj: T, key: K) {
  return obj[key];
}

TypeScript 会推断此函数的返回类型为 T[K],当调用  getProperty 函数时,TypeScript 将推断我们将要读取的属性的实际类型:


const user: User = {
  id: 15,
  name: 'John',
  email: 'john@smith.com',
  role: 'admin'
};
getProperty(user, 'name'); // string
getProperty(user, 'id');   // number

name属性被推断为string类型,age属性被推断为number类型。当访问User中不存在的属性时,就会报错:


getProperty(user, 'property'); // ❌ 类型“"property"”的参数不能赋给类型“keyof User”的参数。


你不知道的 TypeScript 高级类型(下)https://developer.aliyun.com/article/1411325

相关文章
|
2天前
|
监控 JavaScript 安全
TypeScript在员工上网行为监控中的类型安全实践
本文演示了如何使用TypeScript在员工上网行为监控系统中实现类型安全。通过定义`Website`类型和`MonitoringData`接口,确保数据准确性和可靠性。示例展示了从监控设备获取数据和提交到网站的函数,强调了类型定义在防止错误、提升代码可维护性方面的作用。
28 7
|
2天前
|
JavaScript 安全 前端开发
【TypeScript技术专栏】TypeScript中的类型推断与类型守卫
【4月更文挑战第30天】TypeScript的类型推断与类型守卫是提升代码安全的关键。类型推断自动识别变量类型,减少错误,包括基础、上下文、最佳通用和控制流类型推断。类型守卫则通过`typeof`、`instanceof`及自定义函数在运行时确认变量类型,确保类型安全。两者结合使用,优化开发体验,助力构建健壮应用。
|
2天前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript类型系统与接口详解
【4月更文挑战第30天】TypeScript扩展JavaScript,引入静态类型检查以减少错误。其类型系统包括基本类型、数组等,而接口是定义对象结构的机制。接口描述对象外形,不涉及实现,可用于规定对象属性和方法。通过声明、实现接口,以及利用可选、只读属性,接口继承和合并,TypeScript增强了代码的健壮性和维护性。学习和掌握TypeScript的接口对于大型项目开发至关重要。
|
2天前
|
JavaScript 安全 前端开发
【亮剑】TypeScript 由于其强类型的特性,直接为对象动态添加属性可能会遇到一些问题
【4月更文挑战第30天】本文探讨了在 TypeScript 中安全地为对象动态添加属性的方法。基础方法是使用索引签名,允许接受任何属性名但牺牲了部分类型检查。进阶方法是接口扩展,通过声明合并动态添加属性,保持类型安全但可能导致代码重复。高级方法利用 OOP 模式的类继承,确保类型安全但增加代码复杂性。选择哪种方法取决于应用场景、代码复杂性和类型安全性需求。
|
2天前
|
JavaScript 前端开发
TypeScript基础类型
TypeScript基础类型
|
2天前
|
JavaScript 前端开发
typescript 混合类型
typescript 混合类型
|
2天前
|
JavaScript 前端开发 开发者
类型检查:结合TypeScript和Vue进行开发
【4月更文挑战第24天】TypeScript是JavaScript超集,提供类型注解等特性,提升代码质量和可维护性。Vue.js是一款高效前端框架,两者结合优化开发体验。本文指导如何配置和使用TypeScript与Vue:安装TypeScript和Vue CLI,创建Vue项目时选择TypeScript支持,配置`tsconfig.json`,编写`.tsx`组件,最后运行和构建项目。这种结合有助于错误检查和提升开发效率。
|
2天前
|
JavaScript 编译器 开发者
TypeScript中的类型推断机制:原理与实践
【4月更文挑战第23天】TypeScript的类型推断简化编码,提高代码可读性。编译器基于变量初始值或上下文推断类型,若新值不兼容则报错。文章深入探讨了类型推断原理和实践,包括基本类型、数组、函数参数与返回值、对象类型的推断,并提醒注意类型推断的限制,如非万能、类型兼容性和适度显式指定类型。了解这些能帮助更好地使用TypeScript。
|
2天前
|
JavaScript 前端开发 编译器
TypeScript中的高级类型:联合类型、交叉类型与条件类型深入解析
【4月更文挑战第23天】探索TypeScript的高级类型。这些特性增强类型系统的灵活性,提升代码质量和维护性。
|
2天前
|
JavaScript 安全 编译器
TypeScript中类型断言的使用与风险
【4月更文挑战第23天】TypeScript中的类型断言用于显式指定值的类型,但在不恰当使用时可能引发运行时错误或降低代码可读性。