TypeScript - 日常类型

lxf2023-12-17 18:00:02

本文正在参加「」

TypeScript提供了JavaScript中已有的基本类型,同时还提供了额外的数据类型

还可以通过这些类型来创建自定义类型并提高代码的可读性和可维护性

string,number, boolean

TypeScript中有三种非常常用的基本类型:字符串(string)、数字(number)和布尔值(boolean)

类型名称 String、Number 和 Boolean(以大写字母开头)是合法的, 它们对应的是对应基础类型对应的包装类类型,很少使用,所以尽可能的使用 string、number 或 boolean 来表示这些类型

// 布尔类型 -> 只有true和false两个值 -> 可以看成是 true | false
let bool: boolean = true

// 数值类型
// 1. 和JavaScript一样,TypeScript不区分整数和浮点数,一切都是number
// 2. 可以表示 十进制,二进制和八进制, 十六进制数据
let num: number = 123

// 字符串类型
// 支持单引号,双引号 和 模板字符串
let str: string = 'Hello World'

// 默认情况下,TypeScript所有文件都在一个编译作用域中
// 所以需要添加任何模块化代码,以确保每一个JS文件都在一个独立的编译作用域中
export {}

数组,元组

// 数组类型
// 写法一
let arr1: number[] = [1, 2, 3]

// 写法二
let arr2: Array<number> = [4, 5, 6]

export {}
// 元组类型
// 元组类型是特殊的数组 --- 长度固定, 且各个位置的元素类型固定
let tuple: [number, string] = [1, 'foo']

export {}

any

使用any类型可以绕过TypeScript的类型检查系统,使得对于该值的任何操作都是合法的

使用any类型会降低代码的类型安全性,因为编译器不会对其进行类型检查,因此可能会导致潜在的类型错误

在实际开发中,应该尽可能避免使用any类型,而是尽可能地使用明确的类型注释来提高代码的可读性和类型安全性

如果在代码中没有指定类型,并且TypeScript无法从上下文中推断出类型时,编译器会默认使用any类型,这种被称之为隐式any类型, 但是,这种隐式的any类型可能会导致潜在的类型错误

因此为了避免这种情况发生,可以使用编译器标志noImplicitAny来禁止隐式的any类型,并将其标记为错误

let user: any = {
  name: 'Klaus'
}

// 对any类型进行任何操作都是合法的
// 从any类型上所取到的值也是any类型
console.log(user.age)

unknown

unknown 类型表示任何值。这类似于 any 类型,但更安全,因对unknown类型进行的任何操作都是非法的

如果一个值在当前阶段无法推断出具体的类型的时候,建议使用unknown而不是any

function foo(arg: unknown) {
  // 对unknown执行的任何操作都是非法的
  // 如果想对其进行操作,只有类型缩小 将unknown类型转换为更为具体的类型后使用
  if (typeof arg === 'string') {
    return arg.toUpperCase()
  } else if (typeof arg === 'number') {
    return arg.toString()
  } else {
    return arg
  }
}

void

当一个函数没有返回值的时候,这个函数的返回值类型就是void

因为在JavaScript中,如果一个函数没有任何返回值的情况下,默认的返回值类型是undefined

所以undefined是void的子类型,也就意味着一个undefined类型的值可以赋值给一个void类型的值

let foo: (str: string) => void
foo = (str: string) => console.log(str)

export {}

never

never 类型表示永远不会出现的值

在返回类型中,这意味着函数抛出了异常或执行了死循环代码

当 TypeScript 确定联合类型中没有剩余值时,也会出现 never 类型

function fail(msg: string): never {
  throw new Error(msg);
}

never是任意类型的子类型 所以never类型可以赋值给任意类型,但是没有任意类型可以赋值给never类型变量

function fn(arg: string | number) {
  if (typeof arg === "string") {
    // arg为string时候执行的逻辑
  } else if (typeof arg === "number") {
    // arg为numer时候执行的逻辑
  } else {
    // 正常情况下,不会执行到这里的逻辑, 所以这里arg的类型是never
    // 如果下面这行报错了, 就说明arg中有一个联合类型成员没有处理
    let n: never = arg
  }
}

object

特殊类型 object 指的是任何不是原始类型(string、number、bigint、boolean、symbol、null 或 undefined)的值

因为object类型只能确定描述的内容是对象,没有办法描述对象中的具体结构,所以只能使用对象上的公共属性和方法,而无法使用对象上的特有属性和方法

所以object类型基本上是不会被使用的

null, undefined

JavaScript 有两个原始值用于表示缺失或未初始化的值:null 和 undefined

TypeScript 也有同名的两种相应类型, 这两种值在TypeScript中的行为取决于是否开启strictNullChecks

  1. 当strictNullChecks 为 off 时, null 和 undefined 值可以分配给任何类型的属性

    而这往往是错误的主要来源,所以始终建议在实践中打开 strictNullChecks选项

  2. 当strictNullChecks 为 on 时, null 和 undefined 值不可以分配给任何类型的属性

    也就意味着当一个值为 null 或 undefined 时,需要在使用该值的方法或属性之前对这些值进行测试

    即需要进行类型缩小,以排除值为 null 或 undefined的情况

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

函数

在 JavaScript 中,函数是传递数据的主要手段。TypeScript 可以指定函数的输入和输出值的类型

基本使用

参数类型

当你声明一个函数时,你可以在每个参数后面添加类型注释,以声明函数接受的参数类型

如果你没有在参数上添加类型注释,TypeScript 仍然会检查你传递的参数数量是否正确

// 这里的string就是参数类型注解
function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

返回值类型

返回类型注释出现在参数列表之后

通常情况下不需要返回类型注释, TypeScript 会根据函数的返回语句推断其返回类型

有些代码库会出于文档目的、防止意外更改或个人偏好而显式指定返回类型

function getNumber(): number {
  return 26;
}

匿名函数

匿名函数往往会作为另一个函数的参数使用

此时TypeScript可以根据函数的参数类型自动推导出匿名函数的类型,这类类型被称之为上下文类型

const names = ["Alice", "Bob", "Eve"];

// 此时,TypeScript的类型系统可以自动推导出s的类型为字符串
names.forEach(function (s) {
  console.log(s.toUpperCase());
});

类型组合

TypeScript 的类型系统允许使用各种运算符从现有类型构建新类型

联合类型

联合类型是由两个或更多的不同类型共同组合而形成的一种类型

组成联合类型的每一种类型都被称之为联合类型成员

联合类型对应的实际值只需要和联合的任何成员的类型匹配即可

// id对应的值 可以是 number类型 也可以是 字符串类型
let id: number | string

对于联合类型,TypeScript 只允许执行对于联合的每个成员都有效的操作

如果有字符串 | 数字的联合类型,不能使用仅在字符串中可用的方法

解决方法是使用代码缩小联合类型, 当 TypeScript 可以根据代码的结构推断出一个更具体的类型时,就会进行缩小

function printId(id: number | string) {
  // 这里的typeof 就是类型缩小的一种方法,同样的还有Array.isArray方法
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

类型别名和接口

除了原始值,最常遇到的类型就是对象类型, 任何具有属性的 JavaScript 值都可以看成是一个对象

而TypeScript中定义对象结构的方式有两个 接口 和 别名

对于接口和别名,在命名的时候,需要首字母大写 --- 约定俗成

TypeScript 中的类型检查是基于结构类型的,也被称为鸭子类型(duck typing)

这种类型检查方式强调的是对象的结构和属性是否兼容,而不是对象的类型标识符是否相同

即只要两个对象的结构相同,它们就被认为是同一个类型,即使它们的类型标识符不相同

类型别名

类型别名是给一个已有类型起一个更方便使用的例子

当我们需要多次使用相同的类型, 可以给重复使用的类型起一个别名以方便使用

注意:

  1. 类型别名 只是给一个类型起别名,并不是创建一个全新的类型
  2. 类型别名 可以给任何类型起别名,包括接口,联合类型,甚至是原生基础类型
type Point = {
  x: number;
  y: number;
};

type ID = number | string;
type Num = number;

接口

TypeScript 的接口可以用来创建新的对象类型

  1. 可以使用逗号或分号来分隔属性,最后一个分隔符无论哪种方式都是可选的
  2. 每个属性的类型部分也是可选的。如果您不指定类型,它将被假定为any类型
interface Point {
  x: number;
  y: number;
}

可选属性

对象类型还可以指定它们的一些或全部属性是可选的。要实现这一点,只需在属性名称后面添加?即可

interface Point { 
  x: number; 
  y?: number; // 属性y的值是可选的
}

在 JavaScript 中,如果访问不存在的属性,将得到值为 undefined 而不是运行时错误

因此,当从一个可选属性中读取值时,必须在使用它之前检查是否为 undefined

let point: Point = { x: 100, y: 200 }
console.log(point?.y)

只读属性

当属性前存在readonly修饰符的时候,这个属性就是只读的,即属性值在第一次赋值后,属性值就无法再次进行修改

interface Point {
  readonly x: number;
  y: number;
}

接口 vs 别名

类型别名和接口非常相似,在许多情况下,您可以自由地在它们之间选择,接口的几乎所有功能都可以在类型中使用

关键区别在于类型不能重新打开以添加新属性,而接口始终是可扩展的,所以优先推荐使用接口,再推荐使用类型别名

  1. 接口可以通过继承来复用和扩展属性

    类型别名定义后无法再次扩展,如果真的需要扩展属性,需要使用交叉类型

interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}
  1. 类型别名不能参与声明合并,但接口可以
interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}
type Window = {
  title: string
}

// Error: Duplicate identifier 'Window'.
type Window = {
  ts: TypeScriptAPI
}
  1. 接口只能用于声明对象的形状,而不能重命名基本类型
// error
interface Num = number

// success
type Num = number

类型断言

TypeScript类型系统的原则是使用者比类型系统更知道需要使用什么类型

例如如果您正在使用 document.getElementById,TypeScript 只知道它将返回某种 HTMLElement,但使用者可能知道页面将始终返回 HTMLImageElement, 此时就可以类型断言来将类型断言转换为更具体或更不具体的版本

任何一种类型断言会在编译的时候被移除,并不会影响运行时行为

类型断言一共有两种使用方法

  1. as语法
const imgEl = document.getElementById("poster") as HTMLImageElement;
  1. <>语法 --- 在JSX中会被识别为组件 --- 所以<>语法不能在JSX中使用
const imgEl = <HTMLImageElement>document.getElementById("poster");

TypeScript 只允许将类型断言转换为更具体或更不具体的版本

此规则防止了“不可能”的强制转换,但如果需要将类型转换为无关的类型,可以先转换为any或unknown

const num = '23' as any as number

等价于

const num = '23' as unknown as number

非空断言操作符

如果一个值的类型可能为null或undefined,也可能不为null或undefined的时候

可以在不进行任何显式检查的情况下删除类型中的 null 和 undefined

那就是使用非空断言操作符,简单来说就是在任何表达式后面写上 !

非空断言操作符是一种特殊的类型断言,表示该值不是 null 或 undefined

function liveDangerously(x?: number | null) {
	// 这里的! 就是非空类型断言
  console.log(x!.toFixed());
}

字面量类型

当使用let或var定义变量的时候,值是可以改变的,所以会推断成更为宽泛的类型

// typeof name -> string
let name = 'Klaus'

当使用const定义变量的时候,值是不会改变的,所以会推断成更为具体的类型

// typeof name -> 'Klaus'
// 也就是name的值只能为字符串'Klaus' --- 在这里'Klaus'就是字面量类型
const name = 'Klaus'

所以除了一般的字符串类型和数字类型,可以将特定的字符串字面量值或数字字面量值作为类型使用

最为常见的就是布尔类型,布尔类型本质上就是两个字面量类型的联合,即 true | false

然而单个的字面量类型是没有用处的,但是当字面量类型和字面量类型进行了联合,或者字面量类型和非字面量类型进行了联合,就可以表达一个更有用的概念

let alignment: "left" | "right" | "center"

字面量接口

如果给一个变量赋值对象,默认情况下会推导出宽泛的类型,因为默认情况下,TypeScript类型系统会认为该对象的值在未来某个时候会被修改

// typeof req -> { url: string, method: string }
const req = { url: "https://example.com", method: "GET" };

但有的时候我们明确知道该对象不会再被修改或者需要TypeScript将其中的属性值推导为字面量类型(如这里的method对应的属性值)

  1. 类型断言
const req = { url: "https://example.com", method: "GET" as "GET" };
  1. 字面量接口

    当一个对象后面加上as const的时候,TypeScript类型系统就会明确知道该对象后期不会再修改,此时就会将其类型推导为一个更为具体的类型

// typeof req -> { url: "https://example.com", method: "GET" }
const req = { url: "https://example.com", method: "GET" } as const;

枚举

枚举是给一组固定的值赋予一些语义,从而使代码更容易理解和维护

在TypeScript中,枚举是少数几个会影响JavaScript的运行时的语法扩展

TypeScript 提供了基于数字和字符串的枚举

数字枚举

可以使用 enum 关键字来定义一个枚举

如果不指定枚举值,对应的枚举值会从0开始逐步递增

// 枚举名推送首字母大写
// 枚举成员名 有两个写法 1. 全部大写 2. 首字母大写
enum Direction {
  Up, // => 0
  Down, // => 1
  Left, // => 2
  Right, // => 3
}

该枚举在运行时会被编译为

"use strict";
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

这也就意味着 数值枚举值 有两种使用方式

console.log(Direction.Up) // 普通使用
console.log(Direction[0]) // 反向映射

字符串枚举

字符串枚举类似于数值枚举,只不过每个成员都必须使用字符串字面量或另一个字符串枚举成员进行常量初始化

enum Direct {
  Right = 'RIGHT'
}

enum Direction {
  // 使用字符串字面量进行初始化
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  // 也可以使用另一个已经存在的枚举成员进行初始化
  Right = Direct.Right,
}

在运行时会被编译为

// 编译后会自动开启严格模式
"use strict";
var Direction;
(function (Direction) {
  	// 这也就意味着字符串枚举并不存在反向映射
    Direction["Up"] = "UP";
    Direction["Down"] = "DOWN";
    Direction["Left"] = "LEFT";
    Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));

异构枚举

枚举可以混合使用字符串成员和数字成员,但这是不被推荐的

既有字符串成员 又有数字成员的枚举被称之为异构枚举

enum BoolEnum {
  No = 0,
  Yes = "YES",
}

枚举成员分类

如果枚举成员的值可以在编译时完全确定,不需要调用方法或属性进行计算,那么它就被认为是常量枚举成员,反之其余的会被视为计算枚举成员

enum FileAccess {
  // constant members
  None,
  Read = 'Read',
  ReadWrite = Read | Write,
  // computed member
  G = "123".length,
  F = getAccess()
}

不常用类型

bigint

从ES2020开始,JavaScript中引入了一种用于非常大的整数的原语,即BigInt

const oneHundred: bigint = BigInt(100);
const anotherHundred: bigint = 100n;

symbol

JavaScript中有一种通过Symbol()创建全局唯一引用的原语 (Primitive, 原始数据类型)

const firstName: symbol = Symbol("name");

ps:

Symbol 和 bigInt 都是基础数据类型,而在 TypeScript 中,原始数据类型的名称都是小写的,例如 numberstringboolean

所以Symbol在TypeScript中的类型为symbol,bigInt在TypeScript中的类型为bigint --- 全都是小写的

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!