3.TypeScript常用类型
3.TypeScript常用类型
3.1 概述
TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了:类型系统
- 所有的 JS 代码都是 TS 代码
- JS 有类型(比如,number/string 等),但是 JS 不会检查变量的类型是否发生变化,而 TS 会检查
TypeScript 类型系统的主要优势:可以显示标记出代码中的意外行为,从而降低了发生错误的可能性
示例代码:
let age = 18
let age: number = 18
- 说明:代码中的
: number
就是类型注解 - 作用:为变量添加类型约束。比如,上述代码中,约定变量 age 的类型为 number 类型
- 解释:约定了什么类型,就只能给变量赋值该类型的值,否则,就会报错
- 约定了类型之后,代码的提示就会非常的清晰
- 错误演示:
// 错误代码:
// 错误原因:将 string 类型的值赋值给了 number 类型的变量,类型不一致
let age: number = '18'
3.2 TypeScript常见类型
可以将 TS 中的常用基础类型细分为两类:
- JS 已有类型
- 原始类型,简单类型(
string、boolean、number、object、bigint、symbol、null 和 undefined
) - 复杂数据类型(数组,对象,函数等)
- 原始类型,简单类型(
- TS 新增类型
- 联合类型
- 自定义类型(类型别名)
- 接口
- 元组
- 字面量类型
- 枚举
- void
- any
- …
3.3 原始类型
3.3.1 JS的八种内置类型
原始类型:
string、boolean、number、bigint、object、symbol、null 和 undefined
特点:简单,这些类型,完全按照 JS 中类型的名称来书写
number
let decimal: number = 6; let hex: number = 0xf00d; let binary: number = 0b1010; let octal: number = 0o744; let big: bigint = 100n;
boolean
let isDone: boolean = false;
string
let color: string = "blue"; color = 'red'; let fullName: string = `Bob Bobbington`; let age: number = 37; let sentence: string = `Hello, my name is ${fullName}. I'll be ${age + 1} years old next month.`;
其它
let a: null = null let b: undefined = undefined let obj: object = {x: 1} let s: symbol = Symbol()
对象类型:object(包括,数组、对象、函数等对象)。
特点:对象类型,在TS中更加细化,每个具体的对象都有自己的类型语法。
3.3.2注意点
null和undefined
默认情况下 null
和 undefined
是所有类型的子类型。 就是说你可以把 null
和 undefined
赋值给其他类型。
// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined
// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined
// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined
// null和undefined赋值给Symbol
let sym: symbol = Symbol("me");
sym = null
sym= undefined
// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined
// null和undefined赋值给bigint
let big: bigint = 100n;
big = null
big= undefined
如果你在tsconfig.json指定了"strictNullChecks":true
,null
和 undefined
只能赋值给 void
和它们各自的类型。
number和bigint
虽然number
和bigint
都表示数字,但是这两个类型不兼容。
let big: bigint = 100n;
let num: number = 6;
big = num;
num = big;
会抛出一个类型不兼容的 ts(2322)
错误。
3.4 数组类型
数组类型的两种写法:(推荐使用number[]写法)
let numbers: number[] = [1,2,3]
let string: Array<string> = ["a","b","c"]
定义联合类型数组
let arr:(number | string)[];
// 表示定义了一个名称叫做arr的数组,
// 这个数组中将来既可以存储数值类型的数据, 也可以存储字符串类型的数据
arr3 = [1, 'b', 2, 'c'];
定义指定对象成员的数组:
// interface是接口,最后面会讲到
interface Arrobj{
name:string,
age:number
}
let arr3:Arrobj[]=[{name:'jimmy',age:22}]
3.5 联合类型与交叉类型
3.5.1 联合类型
联合类型表示取值可以为多种类型中的一种,使用 |
分隔每个类型。
需求:数组中既有 number 类型,又有 string 类型,这个数组的类型应该如何写?
let arr: (number | string)[] = [1, 'a', 3, 'b']
- 解释:
|
(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种 - 注意:这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(|| 或)混淆了
let timer: number | null = null
timer = setInterval(() => {}, 1000)
// 定义一个数组,数组中可以有数字或者字符串, 需要注意 | 的优先级
let arr: (number | string)[] = [1, 'abc', 2]
联合类型通常与 null
或 undefined
一起使用:
const sayHello = (name: string | undefined) => {
/* ... */
};
例如,这里 name
的类型是 string | undefined
意味着可以将 string
或 undefined
的值传递给sayHello
函数。
sayHello("semlinker");
sayHello(undefined);
通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
以上示例中的 1
、2
或 'click'
被称为字面量类型,用来约束取值只能是某几个值中的一个。
3.5.1 交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用&
定义交叉类型。
{
type Useless = string & number;
}
很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Useless 的类型就是个 never。
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:
type IntersectionType = { id: number; name: string; } & { age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
在上述示例中,我们通过交叉类型,使得 IntersectionType 同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。
3.6 类型别名
类型别名(自定义类型)
:类型别名用来给一个类型起个新名字。(类型别名常用于联合类型。)- 使用场景:当同一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用
type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
- 解释:
- 使用
type
关键字来创建自定义类型 - 类型别名(比如,此处的 CustomArray)可以是任意合法的变量名称
- 推荐使用大写字母开头
- 创建类型别名后,直接使用该类型别名作为变量的类型注解即可
- 使用
注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
3.7 函数类型
3.7.1 基本使用
- 函数的类型实际上指的是:
函数参数
和返回值
的类型 - 为函数指定类型的两种方式:
- 单独指定参数、返回值的类型
- 同时指定参数、返回值的类型
- 单独指定参数、返回值的类型:
// 函数声明
function add(a: number, b: number): number {
return a + b
}
// 函数表达式
let mySum: (x: number, y: number) => number = function (x, y) {
return x + y;
};
// 箭头函数
const add = (a: number, b: number): number => {
return a + b
}
- 同时指定参数、返回值的类型:
type AddFn = (a: number, b: number) => number
const add: AddFn = (a, b) => {
return a + b
}
- 解释:当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型
- 注意:这种形式只适用于函数表达式
- 用接口定义函数类型
interface ISearchFunc{
(source: string, subString: string): boolean;
}
采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
3.7.2 void 类型
void
表示没有任何类型,和其他类型是平等关系,不能直接赋值:let a: void; let b: number = a; // Error
你只能为它赋予
null
和undefined
(在strictNullChecks
未指定为true时)。声明一个void
类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。如果函数没有返回值,那么,函数返回值类型为:
void
function greet(name: string): void {
console.log('Hello', name)
}
- 值得注意的是,方法没有返回值将得到
undefined
,但是我们需要定义成void
类型,而不是undefined
类型。否则将报错:
// 如果什么都不写,此时,add 函数的返回值类型为: void
const add = () => {}
// 这种写法是明确指定函数返回值类型为 void,与上面不指定返回值类型相同
const add = (): void => {}
function fun(): undefined {
console.log("this is TypeScript");
};
fun(); // Error
// 但,如果指定 返回值类型为 undefined,此时,函数体中必须显示的 return undefined 才可以
const add = (): undefined => {
// 此处,返回的 undefined 是 JS 中的一个值
return undefined
}
3.7.3 never 类型
- void 用来表示空,以函数为例,就表示没有返回值的函数
- never 类型表示的是那些永不存在的值的类型。
值会永不存在的两种情况:
- 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了);
- 函数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。
// 异常
function err(msg: string): never { // OK
throw new Error(msg);
}
// 死循环
function loopForever(): never { // OK
while (true) {};
}
never
类型同null
和undefined
一样,也是任何类型的子类型,也可以赋值给任何类型。
但是没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外),即使any
也不可以赋值给never
let ne: never;
let nev: never;
let an: any;
ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => { throw new Error("异常"); })(); // OK
ne = (() => { while(true) {} })(); // OK
在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:
type Foo = string | number | boolean;
然而他忘记同时修改 controlFlowAnalysisWithNever
方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean
类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保controlFlowAnalysisWithNever
方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
比较:void
和never
// void 用来表示空,以函数为例,就表示没有返回值的函数
function fn(): void {
console.log('hello');
return undefined;
return;
}
// never 表示永远不会返回结果
function fn1(): never {
throw new Error('出错啦!');
}
3.7.4 可选参数
- 使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到可选参数了
- 比如,数组的 slice 方法,可以
slice()
也可以slice(1)
还可以slice(1, 3)
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
- 可选参数:在可传可不传的参数名称后面添加
?
(问号) - 注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数
3.7.5 参数默认值
function buildName(firstName: string, lastName = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
默认值会自动判断类型
3.7.6 剩余参数
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
3.7.7 函数重载
由于 JavaScript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
由于 TypeScript 是 JavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny
的配置项时,以上代码会提示以下错误信息:
Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
该信息告诉我们参数 x 和参数 y 隐式具有 any
类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add
函数同时支持 string 和 number 类型,因此我们可以定义一个 string | number
联合类型,同时我们为该联合类型取个别名:
type Combinable = string | number;
在定义完 Combinable 联合类型后,我们来更新一下 add
函数:
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
为 add
函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add
函数就完美了么,我们来实际测试一下:
const result = add('Semlinker', ' Kakuqo');
result.split(' ');
在上面代码中,我们分别使用 'Semlinker'
和 ' Kakuqo'
这两个字符串作为参数调用 add 函数,并把调用结果保存到一个名为 result
的变量上,这时候我们想当然的认为此时 result 的变量的类型为 string,所以我们就可以正常调用字符串对象上的 split
方法。但这时 TypeScript 编译器又出现以下错误信息了:
Property 'split' does not exist on type 'number'.
很明显 number
类型的对象上并不存在 split
属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript 提供的函数重载特性。
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
type Types = number | string
function add(a:number,b:number):number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a:Types, b:Types) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');
在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string
类型。
3.8 类型推断
{
let str: string = 'this is string';
let num: number = 1;
let bool: boolean = true;
}
{
const str: string = 'this is string';
const num: number = 1;
const bool: boolean = true;
}
看着上面的示例,可能你已经在嘀咕了:定义基础类型的变量都需要写明类型注解,TypeScript 太麻烦了吧?在示例中,使用 let 定义变量时,我们写明类型注解也就罢了,毕竟值可能会被改变。可是,使用 const
常量时还需要写明类型注解,那可真的很麻烦。
实际上,TypeScript 早就考虑到了这么简单而明显的问题。
- 在很多情况下, TS 某些没有明确指出类型的地方,TS 的类型推断机制会帮助提供类型
- 换句话说:由于类型推断的存在,这些地方,类型注解可以省略不写
- 发生类型推断的 2 种常见场景:
- 具有初始化值的变量
- 有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。
{
let str = 'this is string'; // 等价
let num = 1; // 等价
let bool = true; // 等价
}
{
const str = 'this is string'; // 不等价
const num = 1; // 不等价
const bool = true; // 不等价
}
我们能根据 return 语句推断函数返回的类型,如下代码所示:
{
/** 根据参数的类型,推断出返回值的类型也是 number */
function add1(a: number, b: number) {
return a + b;
}
const x1= add1(1, 1); // 推断出 x1 的类型也是 number
/** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
function add2(a: number, b = 1) {
return a + b;
}
const x2 = add2(1);
const x3 = add2(1, '1'); // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined
}
我们把 TypeScript 这种基于赋值表达式推断类型的能力称之为类型推断
。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成
any
类型而完全不被类型检查:let myFavoriteNumber; myFavoriteNumber = 'seven'; myFavoriteNumber = 7;
- 推荐:能省略类型注解的地方就省略(
偷懒,充分利用TS类型推断的能力,提升开发效率) - 技巧:如果不知道类型,可以通过鼠标悬停在变量名称上,利用 VSCode 的提示来查看类型
3.9 对象类型
3.9.1 基本使用
- JS 中的对象是由属性和方法构成的,而 TS 对象的类型就是在描述对象的结构(有什么类型的属性和方法)
- 对象类型的写法:
// 空对象
let person: {} = {}
// 有属性的对象
let person: { name: string } = {
name: '同学'
}
// 既有属性又有方法的对象
// 在一行代码中指定对象的多个属性类型时,使用 `;`(分号)来分隔
let person: { name: string; sayHi(): void } = {
name: 'jack',
sayHi() {}
}
// 对象中如果有多个类型,可以换行写:
// 通过换行来分隔多个属性类型,可以去掉 `;`
let person: {
name: string
sayHi(): void
} = {
name: 'jack',
sayHi() {}
}
- 解释:
- 使用
{}
来描述对象结构 - 属性采用
属性名: 类型
的形式 - 方法采用
方法名(): 返回值类型
的形式
- 使用
3.9.2 箭头函数形式的方法类型
- 方法的类型也可以使用箭头函数形式
{
greet(name: string):string
greet: (name: string) => string
}
type Person = {
greet: (name: string) => void;
greet(name: string):void;
}
let person: Person = {
greet(name) {
console.log(name)
}
}
3.9.3 对象可选属性
- 对象的属性或方法,也可以是可选的,此时就用到可选属性了
- 比如,我们在使用
axios({ ... })
时,如果发送 GET 请求,method 属性就可以省略 - 可选属性的语法与函数可选参数的语法一致,都使用
?
来表示
type Config = {
url: string
method?: string
}
function axios(config: Config) {
console.log(config)
}
3.9.4 使用类型别名
- 注意:直接使用
{}
形式为对象添加类型,会降低代码的可读性(不好辨识类型和值) - 推荐:使用类型别名为对象添加类型
// 创建类型别名
type Person = {
name: string
sayHi(): void
}
// 使用类型别名作为对象的类型:
let person: Person = {
name: 'jack',
sayHi() {}
}
3.9.5 任意属性
有时候我们希望一个对象中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
// [propName: string]: any 表示任意类型的属性
let c: {name: string, [propName: string]: any};
c = {name: '猪八戒', age: 18, gender: '男'};
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
3.10 元组类型
3.10.1 元祖定义
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组最重要的特性是可以限制数组元素的个数和类型
,它特别适合用来实现多值返回。
- 场景:在地图中,使用经纬度坐标来标记位置信息
- 可以使用数组来记录坐标,那么,该数组中只有两个元素,并且这两个元素都是数值类型
let position: number[] = [116.2317, 39.5427]
- 使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字
- 更好的方式:
元组 Tuple
- 元组类型是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型(元祖用于保存定长定数据类型的数据)
// 类型必须匹配且个数必须为2
let position: [number, number] = [39.5427, 116.2317]
- 解释:
- 元组类型可以确切地标记出有多少个元素,以及每个元素的类型
- 该示例中,元素有两个元素,每个元素的类型都是 number
注意,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。如果一个数组中可能有多种类型,数量和类型都不确定,那就直接any[]
3.10.2 元祖类型的解构赋值
我们可以通过下标的方式来访问元组中的元素,当元组中的元素较多时,这种方式并不是那么便捷。其实元组也是支持解构赋值的:
let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${id}`);
console.log(`username: ${username}`);
以上代码成功运行后,控制台会输出以下消息:
id: 1
username: Semlinker
这里需要注意的是,在解构赋值时,解构数组元素的个数是不能超过元组中元素的个数,否则也会出现错误,比如:
let employee: [number, string] = [1, "Semlinker"];
let [id, username, age] = employee;
在以上代码中,我们新增了一个 age 变量,但此时 TypeScript 编译器会提示以下错误信息:
Tuple type '[number, string]' of length '2' has no element at index '2'.
很明显元组类型 [number, string]
的长度是 2
,在位置索引 2
处不存在任何元素。
3.10.3 元组类型的可选元素
与函数签名类型,在定义元组类型时,我们也可以通过 ?
号来声明元组类型的可选元素,具体的示例如下:
let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${optionalTuple}`);
在上面代码中,我们定义了一个名为 optionalTuple
的变量,该变量的类型要求包含一个必须的字符串属性和一个可选布尔属性,该代码正常运行后,控制台会输出以下内容:
optionalTuple : Semlinker,true
optionalTuple : Kakuqo
那么在实际工作中,声明可选的元组元素有什么作用?这里我们来举一个例子,在三维坐标轴中,一个坐标点可以使用 (x, y, z)
的形式来表示,对于二维坐标轴来说,坐标点可以使用 (x, y)
的形式来表示,而对于一维坐标轴来说,只要使用 (x)
的形式来表示即可。针对这种情形,在 TypeScript 中就可以利用元组类型可选元素的特性来定义一个元组类型的坐标点,具体实现如下:
type Point = [number, number?, number?];
const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点
console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3
3.10.4 元组类型的剩余元素
元组类型里最后一个元素可以是剩余元素,形式为 ...X
,这里 X
是数组类型。剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string[]]
表示带有一个 number
元素和任意数量string
类型元素的元组类型。为了能更好的理解,我们来举个具体的例子:
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);
3.10.5 只读的元组类型
TypeScript 3.4 还引入了对只读元组的新支持。我们可以为任何元组类型加上 readonly
关键字前缀,以使其成为只读元组。具体的示例如下:
const point: readonly [number, number] = [10, 20];
在使用 readonly
关键字修饰元组类型之后,任何企图修改元组中元素的操作都会抛出异常:
// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);