弱类型和强类型
编程语言,按照数据类型是否固定可分为 强类型语言 和 弱类型语言。
弱类型语言
弱类型语言:变量、函数参数和函数的返回值,是没有类型的,一个变量可以接收任何类型的数据,一个函数也可以返回任何类型的数据。
let n = 10;
n = "abc";
强类型语言
强类型语言:变量,函数参数等,都有固定的类型,在声明变量时必须指定这个变量的类型,一个类型的变量只能接收这个类型的数据。
int n = 10;
n = 10.0; //错误
n = "qqqq" //错误
强类型语言:C/C#/Java等
弱类型:Javascript/lua/python/php等
弱类型语言的优缺点:
- 优点:使用方便、灵活。不需要大量的类型转换。
- 缺点:不够严谨,无法从代码中判断某个变量或参数的类型,不利于多人合作开发大型的项目。
强类型语言的优缺点:
- 优点:逻辑严谨,类型确定,适合开发大型的项目。
- 缺点:在开发过程中需要时刻注意数据的类型,不同类型之间的数据进行计算时需要手动进行转换。
TypeScript
TypeScript简称TS。
TS与JS的关系
- TS和JS之间的关系其实就是Less/Sass和CSS之间的关系
- 就像Less/Sass是对CSS进行扩展一样, TS也是对JS进行扩展
- 就像Less/Sass最终会转换成CSS一样, 我们编写好的TS代码最终也会换成JS
TypeScript简介
TS是JS的超集(扩展集)。它在JS语法额基础上实现了强类型,也就是说TS是强类型的JS,TS扩展了JS,有JS没有的东西。TS仅仅是一个语法标准,它没有执行环境,TS代码最终还要被转为JS代码,在JS执行环境中运行。
- TS是JS的超集。
- TS在JS语法额基础上实现了强类型。
- TS对JS进行了扩展,向JS中引入了类型的概念,并添加了许多新的特性。
- TS代码需要通过编译器编译为JS,然后再交由JS解析器执行。
- TS完全兼容JS,换言之,任何的JS代码都可以直接当成TS使用。
- 相较于JS而言,TS拥有了静态类型,更加严格的语法,更强大的功能;
- TS可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;
- TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;
- 同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS。
为什么需要TypeScript?
- 简单来说就是因为JavaScript是弱类型, 很多错误只有在运行时才会被发现
- 而TypeScript提供了一套静态检测机制, 可以帮助我们在编译时就发现错误
TypeScript特点
- 支持最新的JavaScript新特特性
- 支持代码静态检查
- 支持诸如C,C++,Java,Go等后端语言中的特性 (枚举、泛型、类型转换、命名空间、声明文件、类、接口等)
官网
https://www.tslang.cn/index.html
TypeScript 开发环境搭建
安装TS
下载TS的编译器,它能够把TS文件转为JS文件。
npm install -g typescript
编译TS
手动编译:使用tsc命令将一个TS文件转为JS文件。
tsc index.ts
//如果报错ts-node : 无法将“ts-node”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
//可以试试 npx tsc index.ts 这种运行成功后如果需要使用Code Runner插件的话,需要将插件运行命令更换为npx开头,往下看有更换的步骤
自动编译:编译文件时,使用 -w 指令后,TS编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译。
tsc index.ts -w
//如果不行的话可以试试 npx tsc index.ts -w
vsCode中运行TS
在 vsCode中运行TS文件,也可以直接运行JS文件
-
VSCode安装Code Runner插件
-
全局安装ts-node模块:npm i ts-node -g
-
采用右键
Run Code
或者 快捷键alt+ctrl+n
可以运行TS文件(这里要记得项目路径中不能含有中文字符,否则找不到路径,或者直接报错)(如果输出代码有中文乱码的话,可以去Code Runner插件设置搜索
Run in Terminal
将其选中)(如果路径不含中文还是报错的话,在插件设置随便找个在
setting.json
中编辑点进去找到code-runner.executorMap
选项里面找到typescript
值更换为npx ts-node
保存,这样右键运行时就会按照指定命令运行)
数据类型
类型声明
-
类型声明是TS非常重要的一个特点
-
通过类型声明可以指定TS中变量(参数、形参)的类型
-
指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
-
简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值
let 变量: 类型;
let 变量: 类型 = 值;
function fn(参数: 类型, 参数: 类型): 类型{
...
}
类型
类型 | 例子 | 描述 |
---|---|---|
number | 1, -33, 2.5 | 任意数字 |
string | ‘hi’, “hi”, hi |
可以使用双引号、单引号、模板字符串表示字符串 |
boolean | true、false | 布尔值true或false |
null | null | null有自己的类型叫做null |
undefined | undefined | undefined有自己的类型叫做undefined |
object | {name:‘张三’} | 任意的JS对象 |
array | [1,2,3] | 任意JS数组 |
tuple | [4,5] | 元组,TS新增类型,固定长度数组 |
any | * | 任意类型 |
字面量 | 其本身 | 限制变量的值就是该字面量的值 |
void | 空值(undefined) | 没有值(或undefined) |
never | 没有值 | 不能是任何值 |
unknown | * | 类型安全的any |
enum | enum{A, B} | 枚举,TS中新增类型 |
基本类型
let str: string = "张三";
let num: number = 18;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {name:'张三'};
let big: bigint = 100n;
let sym: symbol = Symbol("foo");
Array
对数组类型的定义有两种方式:使用 Array<类型>
或者 类型[]
两种方式
let names: Array<string> = ['tom', 'lily', 'lucy'];
let ages: number[] = [22, 23, 24];
定义联合类型数组
// 表示定义了一个名称叫做arr的数组,
// 这个数组中将来既可以存储数值类型的数据, 也可以存储字符串类型的数据
let arr:(number | string)[];
arr = [1, 'b', 2, 'c'];
定义指定对象成员的数组:
// interface是接口
interface Arrobj{
name:string,
age:number
}
let arr:Arrobj[] = [{
name:'jimmy',age:22}];
Tuple(元组)
元祖定义
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组最重要的特性是可以限制数组元素的个数和类型
,它特别适合用来实现多值返回。
元祖用于保存定长定数据类型的数据
let x: [string, number];
// 类型必须匹配且个数必须为2
x = ['hello', 10]; // OK
x = ['hello', 10, 10]; // Error
x = [10, 'hello']; // Error
注意,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。如果一个数组中可能有多种类型,数量和类型都不确定,那就直接any[]
元祖类型的解构赋值
我们可以通过下标的方式来访问元组中的元素,当元组中的元素较多时,这种方式并不是那么便捷。其实元组也是支持解构赋值的:
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
处不存在任何元素。
元组类型的可选元素
与函数签名类型,在定义元组类型时,我们也可以通过 ?
号来声明元组类型的可选元素,具体的示例如下:
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
元组类型的剩余元素
元组类型里最后一个元素可以是剩余元素,形式为 ...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]);
只读的元组类型
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);
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("foo");
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) 错误。
void
void
表示没有任何类型,和其他类型是平等关系,不能直接赋值:
let a: void;
let b: number = a; // Error
你只能为它赋予null
和undefined
(在strictNullChecks
未指定为true时)。声明一个void
类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。
值得注意的是,方法没有返回值将得到undefined
,但是我们需要定义成void
类型,而不是undefined
类型。否则将报错:
function fun(): undefined {
console.log("this is TypeScript");
};
fun(); // Error
never
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 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
any
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型。
如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.
但如果是 any
类型,则允许被赋值为任意类型。
let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {
}
在any上访问任何属性都是允许的,也允许调用任何方法.
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let something;
something = 'seven';
something = 7;
something.setName('Tom');
等价于
let something: any;
something = 'seven';
something = 7;
something.setName('Tom');
在许多场景下,这太宽松了。使用 any
类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any
类型,就无法使用 TypeScript 提供的大量的保护机制。请记住,any 是魔鬼!
尽量不要用any。
为了解决 any
带来的问题,TypeScript 3.0 引入了 unknown
类型。
unknown
unknown
与any
一样,所有类型都可以分配给unknown
:
let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK
unknown
与any
的最大区别是:
- 任何类型的值都可以赋值给
any
,同时any
类型的值也都可以赋值给任何类型。 - 任何类型的值都可以赋值给
unknown
,但unknown
只能赋值给unknown
和any
类型,unknown
不能赋值给其他类型
let notSure: unknown = 4;
let uncertain: any = notSure; // OK
let notSure: any = 4;
let uncertain: unknown = notSure; // OK
let notSure: unknown = 4;
let uncertain: number = notSure; // Error
如果不缩小类型,就无法对unknown
类型执行任何操作:
function getDog() {
return '123'
}
const dog: unknown = {
hello: getDog};
dog.hello(); // Error
这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof
、类型断言
等方式来缩小未知范围:
function getDogName() {
let x: unknown;
return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
const upName = dogName.toLowerCase(); // OK
}
// 类型断言
const upName = (dogName as string).toLowerCase(); // OK
Number、String、Boolean、Symbol
首先,我们来回顾一下初学 TypeScript 时,很容易和原始类型 number、string、boolean、symbol 混淆的首字母大写的 Number、String、Boolean、Symbol 类型,后者是相应原始类型的包装对象
,姑且把它们称之为对象类型。
从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。
下面我们看一个具体的示例:
let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)报错
在示例中的第 3 行,我们可以把 number 赋给类型 Number,但在第 4 行把 Number 赋给 number 就会提示 ts(2322) 错误。
因此,我们需要铭记不要使用对象类型来注解值的类型,因为这没有任何意义。
object、Object 和 {}
另外,object(首字母小写,以下称“小 object”)、Object(首字母大写,以下称“大 Object”)和 {}(以下称“空对象”)
小 object 代表的是所有非原始类型,也就是说我们不能把 number、string、boolean、symbol等 原始类型赋值给 object。在严格模式下,null
和 undefined
类型也不能赋给 object。
JavaScript 中以下类型被视为原始类型:
string
、boolean
、number
、bigint
、symbol
、null
和undefined
。
下面我们看一个具体示例:
let lowerCaseObject: object;
lowerCaseObject = 1; // ts(2322)
lowerCaseObject = 'a'; // ts(2322)
lowerCaseObject = true; // ts(2322)
lowerCaseObject = null; // ts(2322)
lowerCaseObject = undefined; // ts(2322)
lowerCaseObject = {
}; // ok
在示例中的第 2~6 行都会提示 ts(2322) 错误,但是我们在第 7 行把一个空对象赋值给 object 后,则可以通过静态类型检测。
大Object 代表所有拥有 toString、hasOwnProperty 方法的类型,所以所有原始类型、非原始类型都可以赋给 Object。同样,在严格模式下,null 和 undefined 类型也不能赋给 Object。
下面我们也看一个具体的示例:
let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {
}; // ok
在示例中的第 2到4 行、第 7 行都可以通过静态类型检测,而第 5~6 行则会提示 ts(2322) 错误。
从上面示例可以看到,大 Object 包含原始类型,小 object 仅包含非原始类型,所以大 Object 似乎是小 object 的父类型。实际上,大 Object 不仅是小 object 的父类型,同时也是小 object 的子类型。
下面我们还是通过一个具体的示例进行说明。
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok
在示例中的第 1 行和第 2 行返回的类型都是 true,第3 行和第 4 行的 upperCaseObject 与 lowerCaseObject 可以互相赋值。
注意:尽管官方文档说可以使用小 object 代替大 Object,但是我们仍要明白大 Object 并不完全等价于小 object。
{}空对象类型和大 Object 一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null 和 undefined 也不能赋给 {} ,如下示例:
let ObjectLiteral: {
};
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // ts(2322)
ObjectLiteral = undefined; // ts(2322)
ObjectLiteral = {
}; // ok
type isLiteralCaseObjectExtendsUpperCaseObject = {
} extends Object ? true : false; // true
type isUpperCaseObjectExtendsLiteralCaseObject = Object extends {
} ? true : false; // true
upperCaseObject = ObjectLiteral;
ObjectLiteral = upperCaseObject;
在示例中的第 8 行和第 9 行返回的类型都是 true,第10 行和第 11 行的 ObjectLiteral 与 upperCaseObject 可以互相赋值,第2~4 行、第 7 行的赋值操作都符合静态类型检测;而第5 行、第 6 行则会提示 ts(2322) 错误。
综上结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。
函数
函数声明
-
函数声明的时候,需要指明参数的类型
-
无返回值的函数可以添加 void 指明函数的返回值为 空
-
调用函数时传递的实参也要和形参类型保持一致
function fn(x: number, y: number): void {
console.log(x + y);
}
function sum(x: number, y: number): number {
return x + y;
}
sum(10, 20);
函数表达式
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
用接口定义函数类型
interface SearchFunc{
(source: string, subString: string): boolean;
}
采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
注意点:可选参数后面不允许再出现必需参数
参数默认值
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
剩余参数
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
函数重载
由于 JavaScript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1"