阮一峰《TypeScript 教程》学习笔记一类型系统、数组、元祖

阮一峰《TypeScript 教程》
本篇文章主要记录浏览阮一峰《TypeScript 教程》书籍的过程中本人不会的一些TypeScript的用法。当然,可以说,我都不会哈哈哈,不过有的用法比较奇葩的我就不记录了,只记录我觉得项目中会用到,比较有实用价值的知识点。不得不说,阮老师写的真的是太好了,清晰易懂,而且特别详细。
typescript 在线编译的网站:
https://www.typescriptlang.org/play

类型系统

any、undefined、null

首字母大写的NumberStringBoolean等在 JavaScript 语言中都是内置对象,而不是类型名称。
undefined 和 null 既可以作为值,也可以作为类型

如果没有声明类型的变量,被赋值为undefinednull,它们的类型会被推断为any

let a = undefined;   // any
const b = undefined; // any
let c = null;        // any
const d = null;      // any

任何其他类型的变量都可以赋值为undefinednull。这并不是因为undefinednull包含在number类型里面,而是故意这样设计,任何类型的变量都可以赋值为undefinednull,以便跟 JavaScript 的行为保持一致。

let age:number = 24;
age = null;      // 正确
age = undefined; // 正确

但是这种情况,在编译阶段不报错,在运行阶段可能会报错,例如

const obj:object = undefined;
obj.toString() // 编译不报错,运行就报错

为了避免这种情况,及早发现错误,TypeScript 提供了一个编译选项strictNullChecks。只要打开这个选项,undefinednull就不能赋值给其他类型的变量(除了any类型和unknown类型)。

这个选项在配置文件tsconfig.json的写法如下:

{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}

Object与object类型

大写的 Object 类型代表 Javascript 里面的广义对象,所有可以转义成对象的数据,都属于 Object 类型,这囊括了几乎所有的值。

let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

以上都是正确的赋值。事实上,除了 nullundefined,其他的数据都可以赋值给 Object 类型。
另外,空对象 {}Object 的简写形式,所以 Object 常常使用 {} 代替

let obj:{};
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

无所不包的 Object 对象既不符合直觉,也不利于使用。所以尽量不要用…
小写的 object 类型指的是狭义的对象,即可以用字面量表示的对象,包含数组、对象、函数,不包含原始值

let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错

值类型

TypeScript 规定,单个值也是一种类型,称为“值类型”。下面示例,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错

let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错

TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型

// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y:string = 'https';

联合类型

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。

联合类型A|B表示,任何一个类型只要属于AB,就属于联合类型A|B

let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确

上面示例中,变量x就是联合类型string|number,表示它的值既可以是字符串,也可以是数值。

如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing),区分该值到底属于哪一种类型,然后再进一步处理。

function printId(
  id:number|string
) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。

交叉类型

交叉类型A&B表示,任何一个类型必须同时属于AB,才属于交叉类型A&B,即交叉类型同时满足AB的特征。交叉类型的主要用途是表示对象的合成。

let obj:
  { foo: string } &
  { bar: string };
obj = {
  foo: 'hello',
  bar: 'world'
};

上面示例中,变量obj同时具有属性foo和属性bar

交叉类型常常用来为对象类型添加新属性。

type A = { foo: number };
type B = A & { bar: number };

type 命令

type命令用来定义一个类型的别名。

type Age = number;
let age:Age = 55;

上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。

别名不允许重名。

别名的作用域是块级作用域

别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。

type World = "world";
type Greeting = `hello ${World}`;

type命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。

typeof 运算符

typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型

JavaScript 里面,typeof运算符只可能返回八种结果,而且都是字符串。

typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n // "bigint"

TypeScript 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。

const a = { x: 0 };
type T0 = typeof a;   // { x: number }
type T1 = typeof a.x; // number

TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式,typeof命令的参数不能是类型

块级类型声明

TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。

if (true) {
  type T = number;
  let v:T = 5;
} else {
  type T = string;
  let v:T = 'hello';
}

类型的兼容

TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。如果类型A的值可以赋值给类型B,那么类型A就称为类型B的子类型(subtype)。类型number就是类型number|string的子类型。

TypeScript 的数组类型

JavaScript 数组在 TypeScript 里面分成两种类型,分别是数组(array)和元组(tuple)。

数组特征:元素的类型相同,但是数量不定

数组有两种写法

let arr:number[] = [1, 2, 3];

数组arr的类型是number[],其中number表示数组成员类型是number

如果数组成员的类型比较复杂,可以写在圆括号里面。

let arr:(number|string)[];

数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。

let arr:Array<number> = [1, 2, 3];
let arr:Array<number|string>;

用尖括号的这种写法本质上属于泛型。

数组的类型推断

如果数组初始的值是空的,那么后续赋值过程会根据数组中元素的值自动推断类型,并且会自动更新类型推断

// 推断为 any[]
const arr = [];
// 推断类型为 number[]
arr.push(123);
// 推断类型为 (string | number)[]
arr.push('abc');

但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。

// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错

只读数组

TypeScript 允许声明只读数组,即不允许变动数组成员,方法是在数组类型前面加上readonly关键字。

const arr:readonly number[] = [0, 1];
arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错

TypeScript 将readonly number[]number[]视为两种不一样的类型,后者是前者的子类型。

这是因为只读数组没有pop()push()之类会改变原数组的方法,所以number[]的方法数量要多于readonly number[],这意味着number[]其实是readonly number[]的子类型。

readonly关键字不能与数组的泛型写法一起使用。

// 报错
const arr:readonly Array<number> = [0, 1];

实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。

const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];

只读数组还有一种声明方法,就是使用“const 断言”。

const arr = [0, 1] as const;
arr[0] = [2]; // 报错

上面示例中,as const告诉 TypeScript,推断类型时要把变量arr推断为只读数组,从而使得数组成员无法改变。

多维数组

TypeScript 使用T[][]的形式,表示二维数组,T是最底层数组成员的类型。

var multi:number[][] =[[1,2,3], [23,24,25]];

表示 multi 是一个二维数组,元素的数据类型为 number

TypeScript 的元组类型

元祖是 TypeScript 特有的数据类型,表示元素的数据类型可以不同的数组。
元祖必须声明每个成员的类型

const s:[string, string, boolean] = ['a', 'b', true];

元祖和数组的区分方式就是:元祖的成员类型在方括号里面,数组的成员类型在方括号外面。
使用元祖时,必须设置成员类型,否则就会被推断为一个数组,例如下面的写法:

// a 的类型为 (number | boolean)[]
let a = [1, true];

会被自动推断为成员类型为 (number | boolean) 的数组。
元祖还可以在结尾的成员类型后面加上问号,表示这个成员可有可无

type myTuple = [
  number,
  number,
  number?,
  string?
];

上面这个元祖,表示最后两个成员是可选的。
由于需要声明每个成员的类型,元祖在大多数情况下成员数量都很少。元祖的成员数量在类型声明的时候就能看出来,超过声明的数量的话就会报错。
但是,元祖的成员类型声明可以使用 … 扩展运算符来声明不限制成员数量的元祖

type NamedNums = [
  string,
  ...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];

上面这个元祖,第一个元素必须是字符串,后面可以有若干个数字类型的成员。
扩展运算符在元素的任意位置都可以,但它的后面只能跟数组或元祖

type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];

只读元祖

元祖也可以是只读的。只读元祖有下面两种写法:

// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>

两种写法都可以得到只读元祖,其中写法二是泛型的写法,用到了工具类型 Readonly<T>
跟数组一样,只读元祖是元祖的父类型,因为元祖比只读元祖拥有更多的方法。因此,元祖可以替代只读元祖,只读元祖不能替代元祖。

type t1 = readonly [number, number];
type t2 = [number, number];
let x:t2 = [1, 2];
let y:t1 = x; // 正确
x = y; // 报错

上面代码中,t1 是只读元祖,t2 是普通元祖。可以把普通元祖赋值给只读元祖,但不可以把只读元祖赋值给普通元祖。

成员数量的推断

如果没有扩展运算符,只有普通成员和可选成员,元祖可以推断出成员的数量。
看以下两段代码:

function f(point: [number, number]) {
  if (point.length === 3) {  // 报错
    // ...
  }
}
function f(
  point:[number, number?, number?]
) {
  if (point.length === 4) {  // 报错
    // ...
  }
}

上述两段代码,由于元祖会自动推断成员数量,判断出数量不可能等于进行判断的数值,代码会直接报错。
如果使用了扩展运算符,元祖就无法推断成员数量了。

const myTuple:[...string[]]
  = ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
  // ...
}

如果使用了扩展运算符,其实是会被当成数组看待,而数组的成员数量是不定的,所以此时无法推断成员数量。
一旦扩展运算符使元祖的成员数量无法确定时,typescript 会把元祖当成数组看待。

扩展运算符与成员数量

扩展运算符会将数组转换成一个用逗号分割的序列,由于数组的成员数量是不定的,使用扩展运算符展开数组的时候,展开的结果的成员数量也是不定的。
这就会导致在函数调用时,如果函数接收的参数数量是一定的,但是传参的时候使用了扩展运算符,在编译的时候就会报错

const arr = [1, 2];
function add(x:number, y:number){
  // ...
}
add(...arr) // 报错

在这里插入图片描述
上述报错,原因是因为 add() 函数只能接受两个参数,而使用扩展运算符传参,typescript 会认为参数数量不确定,因此就报错了。
有些函数是可以接受任意数量的参数的,这种就不会报错,比如 console.log()
解决这个问题有两个办法

  • 把成员数量不定的数组,写成成员数量确定的元祖
const arr:[number, number] = [1, 2];
function add(x:number, y:number){
  // ...
}
add(...arr) // 正确
  • 使用 as const 断言
const arr = [1, 2] as const;

使用这种写法,arr 的类型是 readonly [1,2] 只读类型,可以当做数组,也可以当做元祖。

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值