TypeScript

TypeScript的数据类型

学习TypeScript记录(二)

Yixuan Lang
2021-11-01
7 min

# TypeScript 的数据类型

typescriptjavascript几乎一样,拥有相同的数据类型,另外在javascript基础上提供了更加实用的类型供开发使用。

在开发阶段,可以为明确的变量定义为某种类型,这样typescript就能在编译阶段进行类型检查,当类型不合符预期结果的时候则会出现错误提示。

该篇文章会总结 TypeScript 中的数据类型,并详述其使用。

ts_data_types

TypeScript 的数据类型和 JavaScript 一样也可以分为两个大类分别是:基本类型和复杂类型

# 一、基本类型

# 👉 number 类型

数字类型是我们开发中经常用到的类型,和javascript一样,typescript不区分整数类型(int)和浮点类型(double),统一为number类型。可支持二进制、八进制、十进制和十六进制。

let num: number = 123;
// num = '456'; // 错误
num = 456; //正确

进制表示:

let decLiteral: number = 6; // 十进制
let hexLiteral: number = 0xf00d; // 十六进制
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制

# 👉 boolean 类型

类型为布尔值类型的变量的值只能是true或者false。除此之外,赋值给布尔值的值也可以是一个计算之后结果为布尔值的表达式:

let bool: boolean = false;
bool = true;

let bool: boolean = !!0;
console.log(bool); // false

# 👉 string 类型

字符串类型,和JavaScript一样,可以使用双引号(")或单引号(')表示字符串

let str: string = "Hello World";
str = "Hello World";

作为超集,当然也可以使用模版字符串``进行包裹,通过 ${} 嵌入变量

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }

# 👉 null 和 undefined 类型

在 JavaScript 中,undefinednull是两个基本数据类型。在 TypeScript 中,这两者都有各自的类型,即 undefined 和 null,也就是说它们既是实际的值,也是类型。这两种类型的实际用处不是很大。

let u: undefined = undefined;
let n: null = null;
复制代码;

注意,第一行代码可能会报一个 tslint 的错误:Unnecessary initialization to 'undefined',就是不能给一个变量赋值为 undefined。但实际上给变量赋值为 undefined 是完全可以的,所以如果想让代码合理化,可以配置 tslint,将"no-unnecessary-initializer"设置为false即可。

默认情况下,undefined 和 null 是所有类型的子类型,可以赋值给任意类型的值,也就是说可以把 undefined 赋值给 void 类型,也可以赋值给 number 类型。当在 tsconfig.json 的"compilerOptions"里设置为 "strictNullChecks": true 时,就必须严格对待了。这时 undefined 和 null 将只能赋值给它们自身或者 void 类型。这样也可以规避一些错误。

# 二、复杂类型

# 👉 array 类型

在 TypeScript 中有两种定义数组的方式:

  • 直接定义: 通过 number[] 的形式来指定这个类型元素均为 number 类型的数组类型,推荐使用这种写法。
  • 数组泛型: 通过 Array 的形式来定义,使用这种形式定义时,tslint 可能会警告让我们使用第一种形式定义,可以通过在tslint.json 的 rules 中加入 "array-type": [false] 就可以关闭 tslint 对这条的检测。
let list1: number[] = [1, 2, 3]; // 直接定义
let list2: Array<number> = [1, 2, 3]; // 数组泛型

以上两种定义数组类型的方式虽然本质上没有任何区别,但是==更推荐使用第一种形式来定义==。一方面可以避免与 JSX 语法冲突,另一方面可以减少代码量。

注意,这两种写法中的 number 指定的是数组元素的类型,也可以在这里将数组的元素指定为其他任意类型。如果要指定一个数组里的元素既可以是数值也可以是字符串,那么可以使用这种方式: number|string[]

# 👉 object 类型

在 JavaScript 中,object是引用类型,它存储的是值的引用。在 TypeScript 中,当想让一个变量或者函数的参数的类型是一个对象的形式时,可以使用这个类型:

let obj: object;
obj = { name: "TypeScript" };
obj = 123; // error 不能将类型“123”分配给类型“object”
console.log(obj.name); // error 类型“object”上不存在属性“name”

可以看到,当给一个对象类型的变量赋值一个对象时,就会报错。对象类型更适合以下场景:

function getKeys(obj: object) {
  return Object.keys(obj); // 会以列表的形式返回obj中的值
}
getKeys({ a: "a" }); // ['a']
getKeys(123); // error 类型“123”的参数不能赋给类型“object”的参数

# 👉 any 类型

在编写代码时,有时并==不清楚一个值是什么类型,这时就需要用到any类型==,它是一个任意类型,定义为any类型的变量就会绕过 TypeScript 的静态类型检测。对于声明为any类型的值,可以对其进行任何操作,包括获取事实上并不存在的属性、方法,并且 TypeScript 无法检测其属性是否存在、类型是否正确。

我们可以将一个值定义为 any 类型,也可以在定义数组类型时使用 any 来指定数组中的元素类型为任意类型:

let value: any;
value = 123;
value = "abc";
value = false;

const array: any[] = [1, "a", true];

any 类型会在对象的调用链中进行传导,即 any 类型对象的任意属性的类型都是 any,如下代码所示:

let obj: any = {};
let z = obj.x.y.z; // z的类型是any,不会报错
z(); // success

需要注意:不要滥用 any 类型,如果代码中充满了 any,那 TypeScript 和 JavaScript 就毫无区别了,所以除非有充足的理由,否则应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。

# 👉 void 类型

voidany 相反,any 是表示任意类型,而 void 是表示没有类型,就是什么类型都不是。这在==定义函数,并且函数没有返回值时会用到==:

const consoleText = (text: string): void => {
  console.log(text);
};

需要注意:void 类型的变量只能赋值为 undefined 和 null ,其他类型不能赋值给 void 类型的变量。

# 👉 never 类型

never 类型指永远不存在值的类型,它是那些总会抛出异常根本不会有返回值的函数表达式的返回值类型,当变量被永不为真的类型保护所约束时,该变量也是 never 类型。

下面的函数,总是会抛出异常,所以它的返回值类型是never,用来表明它的返回值是不存在的:

const errorFunc = (message: string): never => {
  throw new Error(message);
};

【注意】never 类型是任何类型的子类型,所以它可以赋值给任何类型;而没有类型是 never 的子类型,所以除了它自身以外,其他类型(包括 any 类型)都不能为 never 类型赋值。

let neverVariable = (() => {
  while (true) {}
})();
neverVariable = 123; // error 不能将类型"number"分配给类型"never"

上面代码定义了一个立即执行函数,函数体是一个死循环,这个函数调用后的返回值类型为 never,所以赋值之后 neverVariable 的类型是 never 类型,当给 neverVariable 赋值 123 时,就会报错,因为除它自身外任何类型都不能赋值给 never 类型。

基于 never 的特性,我们可以把 never 作为接口类型下的属性类型,用来禁止操作接口下特定的属性:

const props: {
  id: number,
  name?: never,
} = {
  id: 1,
};
props.name = null; // error
props.name = "str"; // error
props.name = 1; // error

可以看到,无论给 props.name 赋什么类型的值,它都会提示类型错误,这就相当于将 name 属性设置为了只读

# 👉 tuple 类型

在 JavaScript 中并没有元组的概念,作为一门动态类型语言,它的优势是支持多类型元素数组。但是出于较好的扩展性、可读性和稳定性考虑,我们通常会把不同类型的值通过键值对的形式塞到一个对象中,再返回这个对象,而不是使用没有任何限制的数组。TypeScript 的元组类型正好弥补了这个不足,使得定义包含固定个数元素、每个元素类型未必相同的数组成为可能

元组可以看做是数组的扩展,它表示已知元素数量和类型的数组,它特别适合用来实现多值返回。确切的说,就是已知数组中每一个位置上的元素的类型,可以通过元组的索引为元素赋值::

let arr: [string, number, boolean];
arr = ["a", 2, false]; // success
arr = [2, "a", false]; // error 不能将类型“number”分配给类型“string”。 不能将类型“string”分配给类型“number”。
arr = ["a", 2]; // error Property '2' is missing in type '[string, number]' but required in type '[string, number, boolean]'
arr[1] = 996;

可以看到,定义的 arr 元组中,元素个数和元素类型都是确定的,当为 arr 赋值时,各个位置上的元素类型都要对应,元素个数也要一致

当访问元组元素时,TypeScript 也会对元素做类型检查,如果元素是一个字符串,那么它只能使用字符串方法,如果使用别的类型的方法,就会报错。

在 TypeScript 新的版本中,TypeScript 会对元组做越界判断。超出规定个数的元素称作越界元素,元素赋值必须类型和个数都对应,不能超出定义的元素个数。

在新的版本中,[string, number]元组类型的声明效果上可以看做等同于下面的声明:

interface Tuple extends Array<number | string> {
  0: string;
  1: number;
  length: 2;
}

这里定义了接口 Tuple ,它继承数组类型,并且数组元素的类型是 number 和 string 构成的联合类型,这样接口 Tuple 就拥有了数组类型所有的特性。并且指定索引为 0 的值为 string 类型,索引为 1 的值为 number 类型,同时指定 length 属性的类型字面量为 2,这样在指定一个类型为这个接口 Tuple 时,这个值必须是数组,而且如果元素个数超过 2 个时,它的 length 就不是 2 是大于 2 的数了,就不满足这个接口定义了,所以就会报错;当然,如果元素个数不够 2 个也会报错,因为索引为 0 或 1 的值缺失。

# 👉 enum 类型

TypeScript 在 ES 原有类型基础上加入枚举类型,使得在 TypeScript 中也可以给一组数值赋予名字,这样对开发者比较友好。通俗来说,枚举就是一个对象的所有可能取值的集合枚,举类型使用enum来定义:

enum Roles {
  SUPER_ADMIN,
  ADMIN,
  USER
}

上面定义的枚举类型的 Roles,它有三个值,TypeScript 会为它们每个值分配编号,默认从 0 开始,在使用时,就可以使用名字而不需要记数字和名称的对应关系了:

enum Roles {
  SUPER_ADMIN = 0,
  ADMIN = 1,
  USER = 2
}

const superAdmin = Roles.SUPER_ADMIN;
console.log(superAdmin); // 0
console.log(Roles[1])    // ADMIN

除此之外,还可以修改这个数值,让 SUPER_ADMIN = 1,这样后面的值就分别是 2 和 3。当然还可以给每个值赋予不同的、不按顺序排列的值:

enum Roles {
   SUPER_ADMIN = 1,
   ADMIN = 3,
   USER = 7
}

# 👉 unknown 类型

unknown 是 TypeScript 在 3.0 版本新增的类型,主要用来描述类型并==不确定的变量==。它看起来和 any 很像,但是还是有区别的,unknown 相对于 any 更安全。

对于 any,来看一个例子:

let value: any;
console.log(value.name);
console.log(value.toFixed());
console.log(value.length);

上面这些语句都不会报错,因为 value 是 any 类型,所以后面三个操作都有合法的情况,当 value 是一个对象时,访问 name 属性是没问题的;当 value 是数值类型的时候,调用它的 toFixed 方法没问题;当 value 是字符串或数组时获取它的 length 属性是没问题的。

当指定值为 unknown 类型的时候,如果没有缩小类型范围的话,是不能对它进行任何操作的。总之,unknown 类型的值不能随便操作。那什么是类型范围缩小呢?下面来看一个例子:

function getValue(value: unknown): string {
  if (value instanceof Date) {
    return value.toISOString();
  }
  return String(value);
}

这里由于把 value 的类型缩小为 Date 实例的范围内,所以进行了 value.toISOString(),也就是使用 ISO 标准将 Date 对象转换为字符串。

使用以下方式也可以缩小类型范围:

let result: unknown;
if (typeof result === "number") {
  result.toFixed();
}

关于 unknown 类型,在使用时需要注意以下几点:

  • 任何类型的值都可以赋值给 unknown 类型:
let value1: unknown;
value1 = "a";
value1 = 123;
  • unknown 不可以赋值给其它类型,只能赋值给 unknown 和 any 类型:
let value2: unknown;
let value3: string = value2; // error 不能将类型“unknown”分配给类型“string”
value1 = value2;
  • unknown 类型的值不能进行任何操作:
let value4: unknown;
value4 += 1; // error 对象的类型为 "unknown"
  • 只能对 unknown 进行等或不等操作,不能进行其它操作:
value1 === value2;
value1 !== value2;
value1 += value2; // error
  • unknown 类型的值不能访问其属性、作为函数调用和作为类创建实例:
let value5: unknown;
value5.age; // error
value5(); // error
new value5(); // error

在实际使用中,如果有类型无法确定的情况,要尽量避免使用 any,因为 any 会丢失类型信息,一旦一个类型被指定为 any,那么在它上面进行任何操作都是合法的,所以会有意想不到的情况发生。因此如果遇到无法确定类型的情况,要==先考虑使用 unknown==。

# 参考文章

TypeScript 数据类型

TS 入门篇 | 详解 TypeScript 数据类型