1.前言
TS中的类型主要分为基础类型
、复合类型
和特殊类型
。其中基础类型也就是我们原生JS中所定义的原始数据类型,其中symbol
和bigint
是在es6+中新增的。复合类型,顾名思义,它并非独立存在,而是多种类型的一个集合。特殊类型,则是在TS中所独有的,针对一些特定的场景才存在。具体请参考下图所示:
2. 基础类型
number
数字类型,其数值类型为浮点数,可支持二进制、八进制、十进制和十六进制
const num: number = 1996;
string
字符串类型,通常我们使用双引号或者单引号来表示字符串
const str: string = 'Hello World';
boolean
布尔类型,定义布尔类型后,其传入的值只能是 true 或者是 false
const flag: boolean = true;
bigint
bigint类型是JS中新增的一种基本数据类型,其可以表示超过Number范围的数字。方便我们可以安全地使用更加精准的时间戳,同时在大整数运算时也将不会再出现整数溢出的现象。
如果需要创建BigInt,只需要在数字末尾追加n即可。或者是直接使用BigInt()构造函数
const bigint1: bigint = 999999999999999999n;
const bigint2: bigint = BigInt('9999999999999');
symbol
symbol是ES6新增的一种基本数据类型,它用来表示独一无二的值,通过Symbol函数生成。注意:Symbol前面不能加new
关键字
const sym: symbol = Symbol('描述信息');
undefined和null
在JavaScript中undefined
表示一个没有设置值的变量,而null
则表示什么都没有,是一个只有一个值的特殊类型,表示一个空对象引用。
默认情况下,null和undefined是所有类型的子类型,也就是说我们可以把null和undefined赋值给任意类型。
const str: string | undefined = 'this is a question';
// 定义为null或undefined类型的,可以赋值为null和undefined中的任意一个
const test1: null = undefined;
const test2: undefined = null;
以上都是在没有开启严格的空类型检查
的情况下。如果我们在tsconfig.json
中将strictNullChecks
设置为true
(默认为false
),那么undefined
和null
只能赋值给void
及其自身。
object
对象类型,它用于表示非原始类型。除此之外,它还存在Object
和{}
类型,它们之间存在一定程度的差别:
object
类型用于表示非原始类型,既除number、string、boolean、undefined、null、bigint、symbol外的其他类型;Object
类型是所有Object类的实例的类型;{}
类型描述了一个没有成员的对象
let person: object;
person = {
name: 'Yancy',
age: '18'
}
3. 复合类型
Array
数组类型,通常使用「类型 + 方括号」
的方式来表示,用于对其内容进行约束
const arr1: number[] = [1, 2, 3, 4, 5]
const arr2: string[] = ['a', 'b', 'c']
除此之外,我们还可以通过数组泛型
来表示
const arr: Array<number> = [1, 2, 3, 4, 5]
也可以直接使用接口(interface)
来定义数组类型
interface IStringArray {
[index: number]: string
}
const arr: IStringArray = ['a', 'b', 'c']
元组
元组(Tuple)
就是元素个数和类型固定的数组类型
type Tuple = [number, string]
接口
接口(interface)
可以描述函数、对象、构造器的结构。
// 对象
interface IPerson {
name: string;
age: number;
}
const obj: IPerson = {
name: 'Yancy',
age: 18,
}
// 函数
interface SayHello {
(name: string): string;
}
const func: SayHello = (name: string) => {
return `hello, ${name}`;
}
// 构造器
interface PersonConstructor {
new (name: string, age: number ): IPerson;
}
function createPerson(ctor: PersonConstructor):IPerson {
return new ctor('guang', 18);
}
在我们平时使用的过程中,我们可能并不希望完全按照接口的返回格式来定义类型,可能会存在多一些属性或者少一些属性的情况,这种情况下,我们可以使用可选属性
进行约束,通常以?:
来表示
interface IPerson {
name: string;
age?: number;
}
除此之外,我们有时希望对象中的一些字段只能在创建时被赋值,而不允许中途进行修改,所以我们在使用接口定义类型时,需要使用readonly
来定义只读属性:
interface IPseron {
readonly id: string;
name: string;
age?: number;
}
const yancy = {
id: '123',
name: 'Yancy',
}
对象类型、class类型在TS中也被叫做索引类型,也就是索引了多个元素的类型。对象可以动态添加属性,如果不知道会有什么属性,可以用可索引签名
interface IPerson {
[prop: string]: number | string;
}
const obj: IPerson = {};
obj.name = 'Yancy';
obj.age = 18;
总之,接口可以用来描述函数、构造器、索引类型(对象、class、数组)等复合类型
枚举
枚举类型,代表着一系列值的集合,可以通俗的理解为枚举就是一个字典。例如我们用枚举来定义星期一到星期天:
enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
在TypeScript中会自动为每一值分配编号,默认是从0
开始,我们在使用就不需要再关注具体的值以及索引的对应关系了。
enum Day {
SUNDAY = 0,
MONDAY = 1,
TUESDAY = 2,
WEDNESDAY = 3,
THURSDAY = 4,
FRIDAY = 5,
SATURDAY = 6
}
最终,我们需要通过点的形式
来获取枚举集合中的成员
console.log(Day.SUNDAY) // 0
console.log(Day.MONDAY) // 1
枚举和对象的重要差异在于,对象是单向映射的,我们只能够从键映射到键值。而枚举是双向映射的,我们即可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员。
除了数字枚举与字符串枚举外,还存在常量枚举。常量枚举只是相比其他枚举只是在前面多了一个const
:
const enum Fruits {
Apple,
Bear,
Banana
}
const apple = Fruits.Apple // 0
对于常量枚举而言,我们只能通过枚举成员访问枚举值,而不能通过枚举值访问枚举成员。
4. 特殊类型
any
any
是任意类型,表示其他任何类型都可以赋值给它,它也可以赋值给任何类型(除了never
)。我们可以在赋值过程中任意去改变类型。变量如果在声明时未指定其类型,那么它会被识别为任意值类型。
一旦被声明为any
类型,类型检查器将不会再对这一系列变量进行检查,而是直接让它们通过编译阶段的检查。
let something: any = 'Yancy';
something = 2022;
something = true;
void
void
类型表示没有任何类型。例如没有返回值的函数,其返回值类型为void
。一旦变量的类型被声明为void,那么就只能赋值为undefined
和null
。
function sayHello (): void {
console.log('Hello');
}
const unusable: void = undefined;
never
never
类型表示永远都不存在的值,返回never的函数必须存在无法达到的终点。比如函数抛异常的时候,返回值就是never
。
引用官方的一段话:
The
never
type represents the type of values that never occur. For instance,never
is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns. Variables also acquire the typenever
when narrowed by any type guards that can never be true.
never类型是那些总会抛出异常或者根本就不会有返回值的函数表达式或者箭头函数表达式的返回值类型。当被永远不可能为真的类型保护所约束时,变量也可能是never类型。
function sendError (): never {
throw new Error('异常错误!');
}
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有任何类型是never的子类型或者可以赋值给never(除never自身以外)。即便是any
也不能赋值给never
。
unknown
unknown
表示未知类型,也就是在我们编写代码时还不确定其具体的数据类型是什么。任何类型都可以赋值给它,但是它不可以赋值给别的类型(除unknown
自身以及any
类型外)。
let test1: unknown = 'Hello, World';
test1 = 2022;
let test2: unknown = '哈哈哈';
let test3: unknown = test2;
let test4: number = test2; // 报错
5. 补充
函数
函数的类型签名
函数的类型其实就是描述函数入参类型与函数返回值类型,他们同样使用:
的语法进行类型标注。
function sayHello (name: string): string {
return `Hello, ${name}`;
}
在函数中同样存在类型推导,例如以上的例子,如果不写返回值的类型,它也能被正确推导为string类型。 除了函数声明以外,针对函数表达式也可以进行类型声明:
const sayHello: (name: string) => string = function (name) {
return `Hello, ${name}`;
}
这里的(name: string) => string
,看着它是一个箭头函数,但是在TypeScript中它叫做函数类型签名。但是上面的写法的可读性非常差,通常情况下,我们推荐以下两种写法:
// 方式一
const sayHello = (name: string): number => {
return `Hello, ${name}`;
}
// 方式二
type FuncSayHello = (name: string) => string;
const sayHello: FuncSayHello = (name) => {
return `Hello, ${name}`;
}
void类型
在TypeScript中,如果一个函数没有返回值,即内部没有return语句,那么它的返回类型就会被标记为void而不是undefined,即使它实际的值是undefined。undefined类型是一个实际的、有意义的类型值,而void才代表着空的、没有意义的类型值。
// 没有调用return
function test (): void {}
// 调用了return语句,但是没有返回值
function test (): undefined () {
return undefined;
}
可选参数与rest参数
通常我们在定义一个函数的时候,它的参数并不一定全部是必传的,也可以直接使用。这个时候我们可以使用?
描述一个可选参数:
// 在逻辑内部注入默认值
function person (name: string, age?: number): string {
return `I,m ${name}, ${age || 18} years old.`;
}
// 直接使用可选参数声明默认值
function person (name: string, age: number = 18): string {
return `I,m ${name}, ${age} years old.`;
}
注意:可选参数必须位于必选参数之后。
rest参数实际上就是一个数组,我们可以直接使用数组类型进行标注,或者使用元祖类型标注
// 数组类型
function test(arg1: string, ...rest: any[]) {}
// 元祖类型
function test(arg1: string, ...rest: [number, boolean]) {}
test('Yancy', 26, true);
重载
针对一些复杂函数,可能入参不同,出参也不同,这种情况下,我们应该如何去定义函数的类型呢?这时就轮到函数重载出场了,重载用于实现不同参数输入并且对应不同参数输出的函数,它可以为同一个函数提供多个函数类型,让我们能够清楚的知道传入不同的参数得到不同的结果。
假设我们需要实现一个反转函数reverse,输入数字123时输出反转的数字321,输入字符串hello时则输出反转的字符串olleh。同一个入参存在多种类型,我们可能会想到使用联合类型
实现:
function reverse (x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string'){
return x.split('').reverse().join('');
}
}
但是如果这样实现,我们的类型定义是不够清晰,输入数字时,输出不一定是数字。这时,我们可以使用重载
定义多个reverse的函数类型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse (x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string'){
return x.split('').reverse().join('');
}
}
上面的例子中,我们重复定义了几次reverse函数的类型,前几次都是函数定义,最后一次才是函数实现。
⚠️注意:TypeScript会优先从最前面的函数定义开始匹配,所以多个函数定义如果存在包含关系,需要优先把精确的定义写在前面。
异步函数、Generator函数等类型签名
异步函数、Generator函数、异步Generator函数的类型签名,参数签名基本都一致,而返回值类型则稍微有些区别:
async function asyncFunc(): Promise<void> {}
function* genFunc(): Iterable<void> {}
async function* asyncGenFunc(): AsyncIterable<void> {}
Class
关于Class相关的概念,推荐大家看看阮一峰老师的ECMAScript 6 入门。一个Class主要由构造函数、属性、方法和访问符组成。属性的类型标注类似于变量,而构造函数、方法、存取器的类型标注类似于函数:
class Person {
name: string;
constructor(val: string){
this.name = val;
}
eat (food: string): void {
console.log(`${this.name}吃了${food}`)
}
get nameA(): string {
return `${this.name}+A`
}
set nameA(value: string) {
this.name = `${value}+A`
}
}
注意:setter
方法不允许进行返回值的类型标注。
修饰符
在TypeScript中 我们还可以为Class成员添加修饰符:public
、private
、protected
、readonly
。除readonly以外,其余三个都属于访问性修饰符,而readonly属于操作性修饰符(与interface中的readonly一个意思)。我们先简单了解下它们对应的函数,再看其在TS中的实现:
- public:此类成员在
类
、类的实例
、子类
中都能被访问(默认修饰符) - private:此类成员仅在
类的内部
被访问 - protected:此类成员仅能在
类与子类
中被访问,你可以将类和类的实例当作两个概念,即一旦实例化完成,那就和类没关系了,即不允许再访问受保护的成员
class Person {
private name: string;
constructor(val: string){
this.name = val;
}
protected eat (food: string): void {
console.log(`${this.name}吃了${food}`)
}
public get nameA(): string {
return `${this.name}+A`
}
public set nameA(value: string) {
this.name = `${value}+A`
}
}
注意:我们通常不会为构造函数添加修饰符,而是让它保持默认的 public。
静态成员
在TypeScript中,我们可以使用static
关键字来标识一个成员为静态成员:
class Foo {
static staticHandler() { }
public instanceHandler() { }
}
不同于实例成员,在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler
这种形式进行访问。从中我们可以看到,静态成员直接被挂载在函数体上,而实例成员挂载在原型上,这就是二者的最重要差异:静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类) 。而原型对象上的实例成员则会沿着原型链进行传递,也就是能够被继承。
继承、实现、抽象类
与JavaScript一样,TypeScript中也使用extends
关键字来实现继承。
class Person { }
class Man extends Person { }
以上的两个类,我们可以简单的称为父类与子类,或者称之为基类
与派生类
。派生类中可以访问到使用public
或protected
修饰符的基类成员。
除此之外,还存在一个非常重要的概念:抽象类
。抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等),一个抽象方法描述了这个方法在实际实现中的结构。类的方法和函数非常相似,包括结构等,因此抽象方法其实描述的就是这个方法的入参类型与返回值类型。抽象类使用abstract
关键字声明:
abstract class Person{
abstract name: string;
abstract get personGetter(): string;
abstract personMethod(value: string): string;
}
⚠️注意:抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员。
6. 总结
以前在使用TypeScript的过程中,经常写着写着就变成了anyscript😭😭😭。最近计划重新对TS相关的知识学习一下,同时记录一下学习笔记,方便以后进行查看,当然也希望能够帮助到对相关知识不太熟悉的同学。不足之处,请大家多多指教。