# 一、Ts的常见类型

行则将至,了解Ts不会像我们学习英语那样“痛苦”,如果TypeScript能够解决你的问题,它就值得你去学习吧~

# 1.变量上的类型注解(Type Annotations on Variables)

当你使用constvarlet 声明一个变量时,你可以选择性的添加一个类型注解,显式指定变量的类型:

let myName: string = "yuguang"; // 这表示变量myName的类型是string

不过大部分时候,这不是必须的。因为 TypeScript 会自动推断类型。举个例子,变量的类型可以基于初始值进行推断:

const myName = "yuguang"; // const mgName: "string"

# 2.基本类型

JavaScript的类型分为两种:原始数据类型和对象类型。

原始数据类型包括:

  • 布尔值
  • 数值
  • 字符串
  • null
  • undefined
  • 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。

# 2.1 number、string、boolean

类型名 String ,Number 和 Boolean (首字母大写)用作类型也是合法的,但它们是一些非常少见的特殊内置类型。所以我们总是使用 string ,number 或者 boolean 。

// boolean
const visible: boolean = false;
// number
const age1 = 200;
const age2: number = 100;
// string
const name: string = '余光';
const templateName = `hi ${name}!`

# 2.2 空值、null、undefined

  • JavaScript中没有空值Void的概念,在TypeScript中,可以用void表示没有任何返回值的函数;
  • 声明一个void类型的变量没有什么用,因为你只能将它赋值为undefinednull;
function alertName(): void {
    alert('My name is Tom');
}

const unusable: void = 1;
// warning 不能将类型“number”分配给类型“void”。
  • 在TypeScript中,可以使用 null 和 undefined 来定义这两个原始数据类型,但看起来没什么意义
const u: undefined = undefined;
const n: null = null;

void的区别是,undefinednull是所有类型的子类型。也就是说undefined类型的变量,可以赋值给number类型的变量:

// 严格模式下,会报错
const num: number = undefined;
// 这样也不会报错
const u: undefined;
const num: number = u;

注意:

  1. 声明变量的数据类型为 void 时,非严格模式下,变量的值可以为 undefined 或 null。而严格模式下,变量的值只能为 undefined。
  2. 严格模式下:let num: number = undefined;会报错

# 3.任意值

如果你没有指定一个类型,TypeScript 也不能从上下文推断出它的类型,编译器就会默认设置为 any 类型。

TypeScript有一个特殊的类型any,当你不希望一个值导致类型检查错误的时候,就可以设置为any,先来看下面的代码:

let age = 100;
age = '余光'; // 不能将类型“string”分配给类型“number”

类型检测会提前帮我们规避一些不必要的风险,大家在平时也一定会遇到类型不匹配这样的错误,如果是any类型,则允许被赋值为任意类型。

let age:any = 100;
age = '余光';

# 3.1 任意值的属性和方法

  • 在任意值上访问任何属性都是允许的;
  • 调用任意方法也是允许的;
let anyThing: any = 'todo';
console.log(anyThing.myName); // type: any
console.log(anyThing.myName.firstName); // type: any

anyThing.toString();

可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。

# 3.2 未声明类型的变量在未指定其类型时会被识别为any

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

let something;
something = 'str';
something = 7;
something.name = '余光';

// 等价于
let something: any;
something = 'str';
something = 7;
something.name = '余光';

注意:

经过前面两点(尤其是第二点)的描述,你会发现如果给变量添加了any的声明,那么Typescript就变成了AnyScript了,那岂不是一夜回到了解放前?理论上我们尽量不使用any,就好像才出现了const和let之后,我们进来避免试用var一样。

# 4.数组

在TypeScript中,数组类型有多种定义方式声明一个类似于 [1, 2, 3] 的数组类型,你可以用number[]。这个语法可以适用于任何类型(举个例子,string[] 表示一个字符串数组)。你也可能看到这种写法 Array<number>,是一样的。未来了解泛型时我们再来深入的研究。先来了解一下简单方法吧~

# 4.1 类型+方括号(推荐)

let fibonacci: number[] = [1, 1, 2, 3, 5]; // 此时数组内每个元素都会进行类型检测
fibonacci.push('8'); // 类型“string”的参数不能赋给类型“number”的参数。

# 4.2 数组泛型

我们可以使用数组泛型(Array Generic) Array<elemType> 来表示数组:

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

# 4.3 any在数组中的应用

一个比较常见的做法是,用any表示数组中允许出现任意类型:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

对于any约束的数字,其实就是主动放弃了类型检测~

# 5.对象

除了原始类型,最常见的类型就是对象类型了。定义一个对象类型,我们只需要简单的列出它的属性和对应的类型。

# 5.1 对象属性注释

来看下面一段代码:

**注意:**此时参数的数量不一致,会导致报错,其实这是因为它们的形状不同,之后我们会展开来说~

function getName(obj: { name: string; age: number }): void {
  console.log(obj.name);
  console.log(obj.age);
}

const res = getName({ name: "余光", age: 100 });
// ✅ 
const res2 = getName({ name: "余光" });
// ❌ 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性。

基于这样的场景,对象内的属性支持可选的,及age可能存在

# 5.2 可选属性

对象类型可以指定一些甚至所有的属性为可选的,你只需要在属性名后添加一个 ?:

function getName(obj: { name: string; age?: number }): void {
    console.log(obj.name);
    console.log(obj.age);
}s

getName({ name: "余光" }); // ✅

注意可选属性可以不存在,但仍然不允许添加未定义的属性。

在Ts中,如果你使用一个可选的属性,最好先检查一下它是否是undefined

# 6.函数

在函数是JavaScript中是一等公民,在Ts中,它允许我们指定输入类型和输出类型。

# 6.1 参数类型注解(Parameter Type Annotations)

当你声明一个函数的时候,你可以在每个参数后面添加一个类型注解,声明函数可以接受什么类型的参数。参数类型注解跟在参数名字后面:

函数声明:

function sum(x: number, y: number): number {
  return x + y;
}

// 输入多余的(或者少于要求的)参数,是不被允许的:

sum(1);
// 应有 2 个参数,但获得 1 个。ts(2554)

sum(1, 2, 3); 
// 应有 2 个参数,但获得 3 个

# 6.2 可选参数和参数默认值

前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?与接口中的可选属性类似,我们用?表示可选的参数:

注意:可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了。

function hello(name1: string, name2?: string): void {
  console.log(`hello! ${name1} ${name2 ? "and" + " " + name2 : ""}`);
}

hello("余光1", "余光2"); // hello! 余光1 and 余光2
hello("余光"); // hello! 余光

同样添加默认值也是可以的,TypeScript 会将添加了默认值的参数识别为可选参数:

function hello(name1: string = "余光", name2: string = "yuguang"): void {
  console.log(`hello! ${name1} ${name2 ? "and" + " " + name2 : ""}`);
}

hello(); // hello! 余光 and yuguang
hello("小明"); // hello! 小明 and yuguang

# 6.3 匿名函数

匿名函数有一点不同于函数声明,当 TypeScript 知道一个匿名函数将被怎样调用的时候,匿名函数的参数会被自动的指定类型。

这个例子来自“冴羽大佬”的示例👍

const arr = [1, 2, 3, 4];

arr.forEach((val) => {
    val.toFixed(1);
});
// ✅

arr.forEach((val) => {
    val.split("-");
});
// ❌ 类型“number”上不存在属性“split”。

尽管参数val并没有添加类型注解,但TypeScript根据forEach函数的类型,以及传入的数据的类型,最后推断出了val的类型。

这个过程被称为上下文推断(contextual typing),因为正是从函数出现的上下文中推断出了它应该有的类型。

# 7.联合类型(Union Types)

联合类型(Union Types)表示取值可以为多种类型中的一种。

前面我们提到了基本类型的指定,如果你希望一个变量的类型是可选的可以使用any,但还有更好的方式,即明确指定几个类型

let category: string | number;
category = '余光';
category = 7;
category = true; // ❌ 不能将类型“boolean”分配给类型“string | number”。

联合类型使用 | 分隔每个类型。表示允许设置的类型,这其中每个类型都是联合类型的成员(members)。

注意

当TypeScript不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number[]): number {
    return something.length; // ✅
}
function getLength(something: string | number): number {
    return something.length; // ❌ 类型“string | number”上不存在属性“length”。类型“number”上不存在属性“length”。
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型,所以也会遇到上面问题

let val: string | number;
val = 'seven';
console.log(val.length); // 5
val = 7;
console.log(val.length); // ❌ 类型“number”上不存在属性“length”

# 8.类型别名(type)

类型别名用来给一个类型起个“新”名字。例如:

type hasLen = string | number[]; // isNumber就是新名字,他可能更语义化一些
const arr: hasLen = [1];

又或者,常用语给一个联合类型定义自己的类型别名

type Name = string; // 字符串
type NameResolver = () => string; // 函数
type NameOrResolver = Name | NameResolver; // 联合类型

function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}

甚至还可以套娃

type aaa = string;
type bbb = number;
type ccc = aaa | bbb; // type ccc = string | number

# 9.接口(interface)

接口声明(interface declaration)是命名对象类型的另一种方式:

function getName(person: { name: string; age: number }): void {
  console.log(person.name);
  console.log(person.age);
}

const res = getName({ name: "余光", age: 100 });

不难理解,我们将参数 obj 内部的属性进行了类型注释,并在之后检查对应的类型,再来看看使用了接口后的代码:

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

function getName(person: Person): void {
  console.log(person.name);
  console.log(person.age);
}
const person1 = { name: "余光1", age: 100 };
const person2 = { name: "余光2", age: 200 };

getName(person1); // 余光1 100
getName(person2); // 余光2 200

TypeScript 只关心传递给 printCoord 的值的结构(structure)——关心值是否有期望的属性。

注意:类型别名和接口非常相似,大部分时候,你可以任意选择使用,两者最关键的差别在于类型别名本身无法添加新的属性,而接口是可以扩展的。
TypeScript中 接口是开放式的~

# 9.1 接口扩展

// 通过继承扩展类型
interface Animal {
	name: string
}

interface Bear extends Animal {
	honey: boolean
}

const bear = getBear()
bear.name
bear.honey

// Type
// 通过交集扩展类型
type Animal = {
	name: string
}

type Bear = Animal & {
	honey: boolean
}

const bear = getBear();
bear.name;
bear.honey;

大部分时候,你可以根据个人喜好进行选择。TypeScript 会告诉你它是否需要其他方式的声明。如果你喜欢探索性的使用,那就使用 interface ,直到你需要用到 type 的特性。

# 10.类型断言(Type Assertion)

类型断言(Type Assertion)可以用来手动指定一个值的类型。就是告诉编译器, 你不要帮我们检查了, 相信我,它就是这个类型。

断言就好比解释型强制类型转换,他会告诉你更加具体或者更不具体的类型!

as 类型;

有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,就像这样:

interface fruits {
  name: string;
  getColor(): void;
}
interface person {
  name: string;
  getAge(): void;
}

type fruitsOrPerson = fruits | person;

function getColor(intance: fruitsOrPerson) {
  return intance.getColor();
}
// 类型“fruitsOrPerson”上不存在属性“getColor”。
// 类型“person”上不存在属性“getColor”

上面的例子中,执行intance.getColor 的时候会报错。这是因为在它类型不确定时,我们使用了非共有属性或方法。

此时可以使用类型断言,将实例断言成fruits

interface fruits {
  name: string;
  getColor(): void;
}
interface person {
  name: string;
  getAge(): void;
}

type fruitsOrPerson = fruits | person;

function getColor(intance: fruitsOrPerson) {
  return (intance as fruits).getColor(); // (method) fruits.getColor(): void
}

需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,不合理的使用会导致不可掌控的错误,例如:

interface fruits {
  name: string;
  getColor(): void;
}
interface person {
  name: string;
  getAge(): void;
}

type fruitsOrPerson = fruits | person;

// 通过
function getColor(intance: fruitsOrPerson) {
  return (intance as fruits).getColor();
}

// 执行错误 intance.getColor is not a function
getColor({
  name: "余光",
  getAge: () => {
    return 100;
  },
});

上面的例子编译时不会报错,但在运行时会报错:

使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性(就像上面的例子),以减少不必要的运行时错误。

# 11.字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

type EventNames = "click" | "scroll" | "mousemove";
function handleEvent(ele: Element, event: EventNames) {
  // do something
}

handleEvent(document.getElementById("hello"), "scroll"); // 没问题
handleEvent(document.getElementById("world"), "onmouseout"); // 报错,event 不能为 'onmouseout'

上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

# 12.小思考

  1. 大多数基本类型都不应该主动填写类型注释,应该让Ts自己推导出变量的类型。
  2. 合理的定义类型,会让Ts在更多的地方给你更全面的提示信息

# 写在最后

参考:

欢迎Star⭐️

Last Updated: 2/14/2023, 2:43:41 PM