Javascript 类型缺陷
类型引发的问题
在编程开发中,有一个共识:错误越早发现,就越容易解决。
例如:
- 能在代码编写时发现错误,就不要等到代码编译时才发现(这也是IDE的优势之一)。
- 能在代码编译时发现错误,就不要在代码运行时才发现(类型检测可以帮助我们在这 方面做得很好)。
- 能在开发阶段发现错误,就不要在测试期间发现错误。
- 能在测试期间发现错误,就不要在上线后发现错误。
我们能不能在代码编译期间发现错误。JavaScript 能做到吗? 答案是否定的。
如下示例:
<script>
function getLen(str) {
return str.length;
}
console.log('开始调用函数');
getLen('hello')
getLen();
console.log('调用函数结束');
</script>
浏览器运行结果如图所示:
这是一个常见的错误,很大程度上是因为 JavaScript 在传入参数时没有进行任何限制,只能在运行期间发现错误。 一旦出现这个错误,会影响后续代码的执行,甚至导致整个项目崩溃。
我们如果一旦发现了这个问题,那么也很容易进行修复。特别是在开发大型项目的时候,我们不能能保证自己绝不会出现这样的问题。如果我们调用别人的类库,如何知道所传递的参数是什么类型呢?
然而,如果我们能在 JavaScript中添加很多限制,就可以很好地避免这种问题,例如:
- 可以要求函数 getLen 中的 str 参数是必需的,如果调用者未传递该参数,则在编译 期间就会报错。
- 还可以要求 str 必须是 String 类型的,如果传入其他类型的值,也会在编译期间直接 报错。这样,我们就可以在编译期间发现许多错误,不必等到运行时才去修复。
缺少类型约束
在 JavaScript 编程中,缺少类型约束是一个普遍存在的问题。由于JavaScript 最初在设计时并未考虑类型约束问题,导致前端开发人员常常不关心变量和参数的类型。在必须确定类型 的情况下,我们往往需要使用各种判断和验证。
正因为这种宽松的类型约束,特别是在开发大型的项目的时候,缺少类型约束会带来很多安全隐患,并且不同的开发人员之间也无法建立良好的类型契约。
- 比如,当我们需要实现一个核心类库时,如果没有类型约束,就需要对传入的参数进行各种验证,以确保代码的健壮性。
- 比如,如果我们需要调用别人编写的函数,但对方并没有对函数进行注释,那么我们只能通过阅读函数内部的逻辑来理解它需要传入哪些参数,以及返回值的类型。
为了弥补JavaScript类型约束的缺陷,微软推出了TypeScript,旨在为JavaScript提供类型检查,增加类型约束。
认识 TypeScript
什么是 TypeScript
- TypeScript 是 JavaScript 的超集,它带有类型并编译出干净的 JavaScript 代码。
- TvpeScript支持 JavaScript 的所有特性,并跟随 ECMAScript 标准的发展,因此支持ES6/ES7/ES8 等新语法标准。
- 除了类型约束,TypeScript 还增加了一些语法扩展,例如枚举类型、元组类型等。
- TypeScript 总是与 ES 标准保持同步甚至领先,最终编译成JavaScript 代码,不存在兼容性的问题,不需要依赖 Babel 等工具 。
总结:
- 类型检查
- 语言扩展
- 工具属性
TypeScript 的特点
TypeScript 官方对其特点给出了以下描述
- 1、始于 JavaScript, 归于JavaScript。
- TypeScript 从今天数以百万计的JavaScript开发者所熟悉的语法和语义开始,能够使用现有avaScript 代码和流行的JavaScript 库,并从JavaScript 代码中调用TypeScript 代码。
- TypeScript 可以编译出纯净、简洁的 JavaScript 代码,并且可以在任何浏览器、Node.js环境,以及任何支持 ECMAScript3 (或更高版本)的JavaScript 引擎中运行。
- 2、用于构建大型项目的强大工具
- TypeScript的类型允许JavaScript开发者在开发JavaScript应用程序时使用高效的开发工具和常用操作,比如静态检查和代码重构。
- 类型是可选的,通过类型推断可以使代码的静态验证有很大的不同。类型让用户可以定义软件组件之间的接口,洞察现有JavaScript 库的行为。
- 3、拥有先进的 JavaScript。
- TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自2015 年的ECMAScript 和未来提案中的特性,例如异步功能和装饰器(Decorator), 有助于构建健壮的组件。
- 这些特性为高可靠应用程序开发提供支持,但是会被编译成简洁的ECMAScript 3(或更新版本)的 JavaScript。
- 4、目前,TypeScript 已经在很多地方被应用。
- Vue3 已经使用TypeScript 进行重构。
- 前最流行的编辑器VS Code 也是使用 TypeScript编写的。
- Element Plus 和Ant Design 这些UI 库也是使用TypeScript编写的。
- 微信小程序开发也支持使用 TypeScript 编写。
搭建 TypeScript 的运行环境
TypeScript 的编译环境
TypeScript 的编译过程:
首先我们可以全局安装 TypeScript
npm i typescript -g
安装好 typescript 后,可以查看 tsc 版本
tsc --version
结果会打印如下:
Version 5.4.4
安装好 TypeScript 编译器之后,可以通过 tsc 命令对 TypeScript 代码进行编译,如下示例, main.ts:
function sum(num1:number, num2:number):number{
return num1 + num2;
}
在终端执行以下编译命令 tsc main.ts,上面的 TypeScript 代码最终被编译成以下JavaScript 代码 main.js:
function sum(num1, num2) {
return num1 + num2;
}
TypeScript 的运行环境
默认情况下,如果我们想查看 TypeScript 代码的运行效果,需要手动将 ts 文件编译成 js 文件,然后在HTML 页面中引入该 js 文件,如下步骤:
- 1、通过 tsc 将 TypeScript 编译为 JavaScript 代码。
- 2、在浏览器或者 Node 环境下运行 JavaScript 代码。
其实,可以通过以下两种解决方案来简化上述过程:
- 1.使用 webpack: 使用 webpack 配置 TypeScript 编译环境,并开启一个本地服务。这样就可以直接在浏览器中运行 TypeScript 代码。
- 2.使用 ts-node库:使用ts-node库为 TypeScript的运行提供执行环境。这样就可以通过Nodejs 命令直接执行 TypeScript代码,无须手动编译成JavaScript文件。
webpack 搭建 TypeScript 运行环境
下面描述用 webpack 怎样运行一个 TypeScript 程序的配置过程。
- 1、初始化工程
npm init -y
- 2、全局安装typescript
npm i typescript -g
- 3、tsc -h ts帮助
如果执行上面命令出现:
tsc : 无法加载文件 D:\install\nodejs\tsc.ps1,因为在此系统上禁止运行脚本。
即是存在权限问题,用管理员身份运行vsCode,然后在控制台执行如下命令:
- 执行:get-ExecutionPolicy,会显示 Restricted,表示状态是禁止的;
- 执行:set-ExecutionPolicy RemoteSigned
- 再次执行:get-ExecutionPolicy,会显示 RemoteSigned,表示状态是允许的;
- 4、tsc --init创建配置项
控制台执行命令,会出现一个tsconfig.json文件。
- 5、在工程中新建src/index.ts文件,代码如下:
let hello: string = 'Hello TypeScript'
- 6、编译index.ts文件
执行命令:
tsc ./src/index.ts
在工程中src/index.ts路径下同样生成了一个编译后的文件,即index.js。
即编译后的代码为:
var hello = 'Hello TypeScript';
依赖安装
配置webpack环境
npm i webpack webpack-cli webpack-dev-server -D
安装ts-loader
npm i ts-loader typescript -D
安装html-webpack-plugin
npm i html-webpack-plugin -D
安装clean-webpack-plugin
npm i clean-webpack-plugin -D
安装webpack-merge
npm i webpack-merge -D
工程配置
在工程下新建app-build文件夹,新建如下结构文件:
- app-build
- webpack.base.config.js
- webpack.config.js
- webpack.dev.config.js
- webpack.pro.config.js
webpack.base.config.js代码:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.ts',
output: {
filename: 'app.js'
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
},
module: {
rules: [
{
test: /\.tsx?$/i,
use: [{
loader: 'ts-loader'
}],
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/tpl/index.html'
})
]
}
webpack.dev.config.js:
module.exports = {
devtool: 'eval-cheap-module-source-map',
}
如果不是webpack5,需要调整为:
module.exports = {
devtool: 'cheap-module-eval-source-map'
}
webpack.pro.config.js代码:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
webpack.config.js代码:
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
const devConfig = require('./webpack.dev.config')
const proConfig = require('./webpack.pro.config')
module.exports = (env, argv) => {
let config = argv.mode === 'development' ? devConfig : proConfig
return merge(baseConfig, config)
}
新建index.html模板,src/tpl/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
入口文件src/index.ts示例代码:
let hello: string = 'Hello TypeScript';
document.addEventListener('DOMContentLoaded', () => {
document.write(hello);
});
在package.json中添加启动命令:
"main": "./src/index.ts",
"scripts": {
"start": "webpack-dev-server --mode=development --config ./app-build/webpack.config.js",
"build": "webpack --mode=production --config ./app-build/webpack.config.js"
},
执行:npm run start
使用ts-node 库搭建 TypeScript 运行环境(推荐)
全局安装 ts-node 工具库:
npm i ts-node -g
我们建一个main.ts示例代码,如下:
function sum(num1:number, num2:number):number{
return num1 + num2;
}
console.log(sum(1,2));
运行ts-node 命令:
ts-node main.ts
控制台结果打印为:
3
JavaScript 的数据类型
JavaScript 数据类型和 TypeScript 数据类型存在一一对应的关系,只不过在 TypeScript 中编写代码时多了类型声明。
number 类型
number 数字类型是开发中经常使用的类型。与 JavaScript一样,TypeScript 不区分整数型(int) 和浮点型 (double),而是统一使用 number类型。
示例代码:
// 定义整数型和浮点型
let num1: number = 10;
let num2: number = 10.5;
console.log(num1, num2); // 10 10.5
在 ES6 中可用二进制、八进制、十进制、十六进制来表示数字, TypeScript 也支持这些表示方式,示例代码如下:
// 其他进制表示
let num3: number = 100; // 十进制
let num4: number = 0b100; // 二进制
let num5: number = 0o10; // 八进制
let num6: number = 0x10; // 十六进制
console.log(num3, num4, num5, num6); // 100 4 8 16
boolean 类型
布尔(boolean) 类型只有两个取值: true 和 false。
示例代码:
// boolean 类型表示
let flag: boolean = true;
flag = false;
flag = 10 > 20;
console.log(flag); // false
string 类型
字符串(string) 类型,可以使用单引号或者双引号表示。
// string 类型表示
let msg: string = 'hello world';
msg = 'hello ts';
console.log(msg); // hello ts
// 下面代码 ts 自动推导出对应标识符的类型,一般情况 可以不加申明
const name = 'zhangsan';
const age = 20;
const info = `name is ${name}, age is ${age}`;
console.log(info); // name is zhangsan, age is 20
export {}
上面示例中,通过export 把该TS文件当成一个模块导出处理,避免与全局变量发生冲突。
array 类型
数组 (array) 类型的定义通常有两种方式:
- 直接使用方括号,例如 const arr=[] 。(默认推荐该方式)
- 使用 Array 构造函数,例如 const arr = new Array()。
示例代码:
const names1: string[] = ['a', 'b', 'c']; // 推荐
const names2: Array<string> = ['a', 'b', 'c']; // 不推荐,会与react、JSX 参数冲突
names1.push(1); // 会报错,数组中存放的数据类型是固定 string
names2.push(1); // 会报错,数组中存放的数据类型是固定 string
export {}
还有一种情况,就是我们的数组元素可能既有字符串也有数字类型,也就是后面要讲的联合类型,示例代码如下:
const names3: (string | number)[] = ['a', 'b', 1]; // 推荐
const names4: Array<string | number> = ['a', 'b', 1]; // 不推荐,会与react、JSX 参数冲突
console.log(names3);
console.log(names4);
object 类型
对象(object) 类型可以用于描述一个对象。
示例代码:
// object 类型表示。但是不推荐使用,因为推导不出明确的属性
const info: object = {
name: 'zhangsan',
age: 18
}
console.log(info.name)
export {}
执行的时候会报错,Property 'name' does not exist on type 'object'。
在 TypeScript 中,当你声明一个变量为 object 类型时,TypeScript编译器并不会知道该对象具有哪些属性。object类型是一个非特定的类型,它只表示该变量是一个对象,但不包含关于对象结构的任何信息。因此,当你尝试访问 info.name 时,TypeScript编译器会抛出一个错误,告诉你 name 属性可能不存在于 info 对象上。
要解决这个问题,你可以使用接口(Interface)或者类型别名(Type Alias)来定义一个更具体的类型,该类型包含了你期望的属性。例如:
使用接口:
interface Person {
name: string;
age: number;
}
const info: Person = {
name: 'zhangsan',
age: 18
};
console.log(info.name);
使用类型别名:
type Person = {
name: string;
age: number;
};
const info: Person = {
name: 'zhangsan',
age: 18
};
console.log(info.name); // 正确,不会报错
TypeScript 会自动进行类型推导(推荐):
// TypeScript 会自动进行类型推导(推荐)
const info = {
name: 'zhangsan',
age: 18
}
console.log(info.name)
null和 undefined 类型
在 JavaScript 中,null和undefined是两个基本数据类型。在TypeScript中对应的类型也是null和undefined,这意味着它们既是实际的值,也是自己的类型。
示例代码:
let n1: null = null;
let n2: undefined = undefined;
let n3 = null; // TypeScript 会自动推导为 any 类型
let n4 = undefined; // TypeScript 会自动推导为 any 类型
console.log(n1,n2); // null undefined
console.log(n3,n4); // null undefined
symbol 类型
在ES5中,不可以在对象中添加相同名称的属性,例如:
const person = {
name: 'zhangsan',
name: 'lisi'
}
通常的做法是定义两个不同的属性名字,例如name1 和 name2 。但是,我们可以使用symbol定义相同的属性名字,因为 symbol 函数返回的值是独一无二的。
示例代码:
const s1 = Symbol('name'); // ts 会自动推导类型,无须手动指定
const s2 = Symbol('name'); // ts 会自动推导类型,无须手动指定
const person = {
[s1]: 'zhangsan',
[s2]: 'lisi'
}
console.log(person); // { [Symbol(name)]: 'zhangsan', [Symbol(name)]: 'lisi' }
TypeScript 的数据类型
any 类型
在某些情况下,我们难以确定变量的类型,且类型可能会发生变化。这时可以使用 any 类型。any 类型相当于一种讨巧的 TypeScript 手段,它具有以下特点:
- 可以对 any 类型的变量进行任何操作,包括获取不存在的属性和方法。
- 可以为一个 any 类型的变量赋任意值,如数字或字符串的值。
示例代码:
let msg: any = 'hello';
msg = 123;
msg = true;
msg = [1, 2];
msg = { name: 'zhangsan' };
console.log(msg); // { name: 'zhangsan' }
const arr: any[] = [1, 2, 'hello']; // 数组可存任意类型的数据,但是不推荐
console.log(arr); // [ 1, 2, 'hello' ]
另外,如果在某些情况下需要处理的类型注解过于烦琐,或者在引入第三方库时缺少类型注解,也可以使用 any 进行类型适配。在 Vue3 源码中,也会使用 any 进行某些类型的适配。
unknown 类型
unknown 是 TypeScript 中比较特殊的一种数据类型,用于描述类型不确定的变量。
unknown类型:只能赋值给 any 和 unknown 类型。
示例代码:
function foo() {
return 'abc';
}
function bar() {
return 123;
}
// unknown类型:只能赋值给 any 和 unknown 类型
let flag = true;
let result: unknown;
if(flag) {
result = foo(); // 接收 string 类型
} else {
result = bar(); // 接收 number 类型
}
console.log(result); // abc
// 下面两个赋值会报错,因为 unknown 类型只能赋值给 any 和 unknown 类型
let message: string = result; // 报错, 不能将类型 “unknown” 分配给类型 “string”
let num: number = result; // 报错, 不能将类型 “unknown” 分配给类型 “number”
void 类型
void 通常用于指定一个函数没有返回值,因此其返回值类型为 void。如果函数返回 void 类型,则可以将 null或 undefined 赋值给该函数,即函数可以返回 null 或 undefined。
如下示例:
function sum(num1: number, num2: number) {
console.log(num1 + num2);
}
这个 sum 函数没有指定任何返回类型,因此默认返回类型为 void。
另外,也可以显式指定 sum 函数返回类型为 void,示例代码如下:
function sum(num1: number, num2: number): void {
console.log(num1 + num2);
}
never 类型
never 表示永远不会有返回值的类型,举例如下:
- 如果一个函数陷入死循环或者抛出一个异常,那么这个函数将不会有任何返回值。
- 如果一个函数确实没有返回值,那么使用 void 类型或其他类型作为返回值类型都不合适,这时就可以使用 never 类型。
示例代码:
function loopFoo(): never { // never 类型,说明该函数不会返回任何内容
while(true) {
console.log('死循环了')
}
}
function loopBar(): never {
throw new Error('报错了');
}
never 类型的应用场景,代码如下:
function handleMessage(message: string|number) {
switch (typeof message) {
case 'string':
console.log('string');
break;
case 'number':
console.log('number');
break;
default:
// 当执行这里的代码时,将message 赋值给 never 类型的 check 会报错
// 这样就可以保证,当修改参数的类型之后,一旦出现 case 没有处理到的情况,就会报错
// 例如,参数增加对 boolean 类型的支持时,必须在 case 中编写对应的处理情况,否则报错
const check: never = message;
}
}
handleMessage('abc'); // string
handleMessage(123); // number
handleMessage(true); // 报错, Argument of type 'boolean' is not assignable to parameter of type 'string | number'.
当向 handleMessage函数传递boolean类型时,由于 case 语句中没有处理 boolean 类型的情况,会执行 default 语句将 message 赋值给 never 类型的 check 变量, 这时会报错 。因此,我们需要在switch 语句中添加 case 语句来处理boolean类型的情况,避免出现运行时错误。
handleMessage 函数的 message 参数是联合类型的,后面会介绍该类型。
在实际编程中,你几乎永远不会有一个变量的类型是 never,因为这意味着这个变量永远不会有任何值。
简而言之,const check: never = message; 这行代码的作用是确保 switch 语句处理了message 参数的所有可能类型。如果将来有人修改了 message 的类型但是没有相应地更新 switch语句,TypeScript 编译器会因为 check 的赋值而报错,从而提醒开发者修复这个问题。
tuple 类型
元组(tuple) 类型,即多个元素的组合。
const info: [string, number] = ['zhangsan', 18]; // 元组可以指定数组中每个元素的类型
const name = info[0]; // 获取 zhangsan,并且指定类型是 string 类型
const age = info[1]; // 获取 18,并且指定类型是 number 类型
元组和数组类型的区别:
- 数组中通常建议只存放相同类型的元素,不推荐存放不同类型的元素。
- 元组中的每个元素都有自己独特的类型,对于通过索引值获取到的值,可以确定其对应的类型。
示例代码:
// 数组的弊端:数组中的每个元素都为任意类型
// const info: any[] = ['zhangsan',18];
// const name = info[0]; // 使用 name 时,提示类型为 any
// 元组的特点:可以指定数组中每个元素的类型
const info: [string, number] = ['zhangsan', 18];
const name = info[0]; // 使用name时,提示类型为 string
console.log(name.length);
const age = info[1]; // 使用age时,提示类型为 number
console.log(age.length); // 报错,类型“number”上不存在属性“length”
在开发中,元组类型通常可以作为函数返回值使用,非常方便,示例代码:
function useState(state: any) {
let currentState = state;
const changeState = (newState: any) => {
currentState = newState;
}
const tuple: [any, (newState: any) => void] = [currentState, changeState];
return tuple;
}
const [state, setState] = useState(10);
需要注意的是,还可以使用泛型进一步优化该应用场景。可以将any 类型替换为泛型参数(T),这样解构出来的变量就会有更具体的类型提示。
调整以上代码为泛型:
function useState<T>(state: T): [T, (newState: T) => void] {
let currentState = state;
const changeState = (newState: T) => {
currentState = newState;
}
const tuple: [T, (newState: T) => void] = [currentState, changeState];
return tuple;
}
const [state, setState] = useState(10);
console.log(state); // 类型是 number
console.log(setState(20)) // 类型是 (newState: number) => void
TypeScript 类型的补充
函数的参数和返回值
函数是 JavaScript 中非常重要的组成部分,TypeScript 允许我们指定函数的参数和返回值的
类型。
1.参数的类型注解
在声明函数时,可以在每个参数后添加类型注解,以声明函数接收的参数类型。
示例代码:
function sum(number1: number, number2: number) {
return number1 + number2;
}
当我们调用的时候:
console.log(sum(1,2)); // 3
console.log(sum('1','2')); // 报错 Argument of type 'string' is not assignable to parameter of type 'number'.
当调用 sum 函数时,如果传入的参数类型或数量不正确,就会报错。
2. 返回值的类型注解
我们也可以在函数参数后面添加返回值的类型注解,示例代码如下:
function sum(number1: number, number2: number): number {
return number1 + number2;
}
在 TypeScript 中,返回值的类型注解和变量的类型注解相似,通常不需要显式地指定返回类型。
TypeScript 会根据函数内部的 return 语句推断函数的返回类型。不过,为了增加代码的可读性,有些第三方库会显式指定函数的返回类型。
3. 匿名函数的参数类型
在 TypeScript 中,匿名函数和函数声明之间存在一些不同之处。当 TypeScript 能够确定函数在哪里被调用以及如何被调用时,会自动推断出该函数的参数类型。
示例:
const list = ['hello','world','typescript','vue3'];
list.forEach((item) => { // 将鼠标放在 item 上,会出现类型提示
console.log(item.toUpperCase());
})
上述代码没有明确指定 item 的类型,但是根据推断,item 应该是字符串类型的。因为在 TypeScript 中,根据 forEach 函数和数组的类型,可以推断出 item 的类型。
对象类型
如果希望限定一个函数接收的参数为对象类型,可以用对象类型作为参数。
示例:
function printPoint(point: {x: number, y: number}) {
console.log(point.x);
console.log(point.y);
}
printPoint({x: 10, y: 20});
我们使用一个对象{x:number,y:number} 作为类型:
- 可以向对象中添加属性,并告知 TypeScript 该属性的类型
- 属性之间可以使用,或;分隔,最后一个分隔符是可选的。
- 每个属性的类型也是可选的。如果不指定,那么默认为any 类型。
在对象类型中,我们可以通过在属性名后面添加 问号(?) 的方式来指定某些属性为可选属性。
示例代码:
function printPoint(point: {x: number, y: number, z?: number}) {
console.log(point.x);
console.log(point.y);
console.log(point.z);
}
printPoint({x: 10, y: 20}); // 10, 20, undefined 没有传递Z,所以为undefined
printPoint({x: 10, y: 20, z: 30}); // 10, 20, 30
联合类型
联合类型:
- 联合类型由两个或多个其他类型组成。
- 联合类型中的每个类型被称为联合成员。
示例:
// 将参数 id 指定为联合类型 string|number|boolean
function printID(id: string | number | boolean) {
console.log('id的值:', id);
}
printID('abc');
printID(123);
printID(true);
可以看出,在为联合类型的参数传入值时,只需要确保传入的是联合类型中的某一种类型值即可。
我们获取的这个值可能是联合类型中的任何一种类型,例如 string 、number 或 boolean。 由于无法确定具体类型,因此,如果在代码中调用了某种类型的方法时,如果传入的类型不匹配就会报错。
例如,下面示例调用 string 类型的方法,会报错:
function printID(id: string | number | boolean) {
console.log('id的值:', id.toUpperCase());
}
这时可以缩小联合类型,TypeScript 会根据缩小的代码结构,推断出更具体的类型。
// 将参数 id 指定为联合类型 string|number|boolean
function printID(id: string | number | boolean) {
if(typeof id === 'string') {
console.log('id的值:', id.toUpperCase());
} else {
console.log(id);
}
}
printID('abc');
printID(123);
printID(true);
类型别名
前面介绍了在类型注解中编写对象类型和联合类型,如果需要在多个地方重复使用这些类型,就需要多次编写相同的代码。
为了解决这个问题,可以使用类型别名(TypeAlias)为某个类型起一个名字。
使用 type 用于定义类型别名。
对象类型的别名,示例代码:
// type 用于定义类型别名
type pointType = {
x: number,
y: number,
z?: number
}
// PointType 是对象类型的别名
function printPoint(point: pointType) {
console.log(point.x);
console.log(point.y);
console.log(point.z);
}
我们也可以为联合类型起一个别名,示例代码如下:
type IDType = string | number | boolean;
function printID(id: IDType) {
if(typeof id === 'string') {
console.log('id的值:', id.toUpperCase());
} else {
console.log(id);
}
}
类型断言
有时 TypeScript 无法获取具体的类型信息,这时需要用到类型断言(Type Assertion)。下面介绍常见的类型断言语法。
1. 类型断言 as
在 TypeScript 中,我们可以使用 as 关键字进行类型断言。
例如通过document.getElementById 获取 img 元素,但是 TypeScript 只知道该函数会返回 HTMLElement,并不知道它具体的类型。
示例代码:
const imgEle = document.getElementById("my-img");
imgEle.src="图片地址";
上面代码会报错,提示:类型“HTMLElement”上不存在属性“src”。
这时,可以使用as 关键字进行类型断言,调整后的代码如下:
const imgEle = document.getElementById("my-img") as HTMLImageElement;
imgEle.src="图片地址"; // 不会报错,因为明确断定myEl 是 HTMLImageElement
示例2,如下示例,在 sayHello 函数中使用 as 关键字对 p 进行类型断言:
class Person {}
class Student extends Person {
studying() {
console.log('学生正在学习')
}
}
function sayHello(p: Person) {
// 使用类型断言 as, 将 p 断言为 Student
(p as Student).studying();
}
const stu = new Student();
sayHello(stu);
需要注意的是,TypeScript 只允许将类型断言转换为更具体或者不太具体的类型版本,此规则可以防止不合理的强制转换。
例如,将一个 string 类型断言为 number 类型时会报错:
// 报错:类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。
// 如果这是有意的,请先将表达式转换为 "unknown"
const message = "hello" as number;
示例3,如果希望上述代码编译通过,可以将 message 的类型转换为any 或 unknown类型:
const message = "hello";
const num1: number = (message as unknown) as number; // 未报错
const num2: number = (message as any) as number; // 未报错
console.log(num1,num2); // hello hello
2. 非空类型断言
在 JavaScript 中,经常使用if 语句进行非空判断。而在 TypeScript中,除了if 语句,还可以使用非空类型断言进行非空判断。
非空类型断言(!) 表示可以确定某个标识符一定是有值的,可以跳过 TypeScript 在编译阶段对它的检测。
如下示例:
function messageLength(message?: string) {
console.log(message.length); // 编译报错:'message' is possibly 'undefined'.
}
messageLength('hello');
上述代码在执行 TypeScript编译时可能会出现错误,因为传入的 message 可能为 undefined 类型,这时就无法访问length 属性。
为了解决这个问题,可以使用if语句进行非空判断,也可以使用非空类型断言。
改进后的代码:
function messageLength(message?: string) {
// 1.使用 if 进行非空判断
// if(message) {
// console.log(message.length);
// }
// 2.用非空类型断言,message 为 undefined 时,运行会报错,因为跳过了TypeScript在编译阶段对它的检测
console.log(message!.length); // 断言message 一定有值
}
messageLength('hello');
3. 可选链的使用
可选链是在 ES11(ES2020) 中新增的特性,并不是 TypeScript 独有的。该特性使用可选
链操作符(?.),通常在定义属性或获取值时使用。
可选链的作用:
- 当对象的属性不存在时,会发生短路,直接返回 undefined。
- 当对象的属性存在时,才会继续执行,例如info.friend?.age。
尽管可选链是 ECMAScript 提出的特性,但在 TypeScript 中的使用效果更佳。
示例:
// 为对象类型起一个 Person 别名
type Person = {
name: string,
friend?: {
name: string,
age?: number,
girlFriend?: {
name: string
}
}
}
// 定义一个对象,指定类型为 Person 类型
const info: Person = {
name: '张三',
friend: {
name: '李四',
girlFriend: {
name: '王五'
}
}
}
// 获取info 对象的属性,用到了可选链?.
console.log(info.name);
console.log(info.friend!.name); // 断言 firend不为空,当为空时,运行程序会报错
console.log(info.friend?.age); // 当 friend 不为空,才取 age。类似 if 语句判空
console.log(info.friend?.girlFriend?.name); // 当friend 、grilFriend 都不为空时,才获取 name
运行结果:
张三
李四
undefined
王五
4. !!操作符
!!操作符可以将一个其他类型的元素转换成 boolean 类型,类似于Boolean (变量)这种函
数转换的方式。
示例代码:
const message = 'hello world';
// 以前转换 boolean 类型的方式
// const flag = Boolean(message);
// console.log(flag);
// 使用!!操作符转成boolean 类型
const flag = !!message;
console.log(flag);
5. ??操作符
??操作符是ES11 新增的空值合并操作符,用于判断一个值是否为 null 或 undefined 。
当左侧操作数为 null 或 undefined 时,返回右侧操作数,否则返回左侧操作数。该操作符可以简化代码,并提高代码的可读性。
示例:
let message: string | null = 'hello';
// 以前的方式是使用三元运算符判空,赋默认值(会判断 null、undefined、' '、false 为假)
const message1 = message ? message : 'world';
// 使用 || 操作符判空,赋默认值(会判断 null、undefined、' '、false 为假)
const message2 = message || '天才啊';
// 使用 ?? 操作符判空,赋默认值(只判断null 或 undefined 为假)
const message3 = message ?? '张三啊';
console.log(message1,message2,message3); // hello hello hello
字面量类型
在 TypeScript 中,除了上述类型,还可以使用字面量类型 (Literal Type)。
示例:
// 'hello world' 也可作为一种类型,叫作字面量类型
let message: 'hello world' = 'hello world';
// message = 'hello'; // 报错:Type '"hello"' is not assignable to type '"hello world"'.
message = 'hello world';
// 10 也可作为一种类型,叫作字面量类型
let num: 10 = 10;
进行字面量类型赋值时,只能赋字面量类型的值。 一般情况下,这样做没有太大的意义,但是如果将多个类型联合在一起,就可以获得更有意义的结果。 示例代码:
// 体现字面量类型的意义,必须结合联合类型
type Direction = 'down' | 'up';
let direction: Direction = 'down';
direction = 'up';
// direction = 'center'; // Type '"center"' is not assignable to type 'Direction'.
在上述代码中,指定 direction 为 Direction 类型,该类型是由多个字面量类型联合在一起形成的联合字面量类型。这样我们就可以限制direction变量的取值为 down和 up,如果赋其他值,将会报错。
在 TypeScript 中,除了自定义字面量类型,还可以利用TypeScript 的字面量推理功能。如下示例:
type Method = 'GET' | 'POST';
function request(url: string, method: Method) {
console.log(url, method);
}
const options = {
url: 'https://www.baidu.com',
method: 'POST'
}
// 参数二报错:options.method 推导出了 string 类型,但是需要 Method 类型
request(options.url, options.method);
上述代码在调用 request 函数时会报错,这是因为 options 对象进行自动类型推导时,推导出了一个{url:string,method:string} 类型,所以无法将一个 string 类型的 options.method 赋值给一个字面量类型的 Method,从而导致报错。
用如下三种方式进行解决上面示例存在的问题。
方式一,使用类型断言 as:
// 方式一:使用类型断言 as
// request(options.url, options.method as Method);
方式二,为 options 指定类型:
// 方式二:为 options 指定类型
type Reqeust = {
url: string,
method: Method
}
const options: Reqeust = {
url: 'https://www.baidu.com',
method: 'POST'
}
request(options.url, options.method)
方式三,使用字面量推理 as const:
// 方式三:使用字面量推理 as const
const options = {
url: 'https://www.baidu.com',
method: 'POST'
} as const; // 将 options 对象的类型推导为字面量类型
request(options.url, options.method);
当你使用 as const 断言时,TypeScript 会更精确地推断这些属性的类型。具体来说:
- url 的类型会被推断为 'https://www.baidu.com' 字面量类型。
- method 的类型会被推断为 'POST' 字面量类型。
这意味着之后你不能给 options.url 赋除 'https://www.baidu.com'以外的任何字符串值,也不能给 options.method 赋除 'POST'以外的任何字符串值。如果尝试这样做,TypeScript 编译器会抛出一个类型错误。
类型缩小
在 TypeScript中,缩小类型的过程被称为类型缩小。我们可以使用类似 typeof num === "number" 的条件语句改变 TypeScript 的执行路径。在特定的执行路径中,可以缩小变量的类型,使其比声明时更为精确。
我们编写的 typeof num === "number" 这种缩小类型的代码就是一种类型保护 (Type Guard) 代码。
常见的类型保护如下:
- typeof:如 typeof num === "number"。
- 平等缩小:如 === 、== 、!== 、!= 、switch。
- instanceof: 如 d instanceof Date。
- in:如'name' in {name: 'zhangsan' }。
1. typeof
JavaScript 支持 typeof 运算符,可以使用该运算符来检查值的类型。在 TypeScript 中,可以通过检查值的类型来缩小不同分支中的类型,这种检查值的类型称为类型保护。
示例代码:
type IDType = string | number | boolean;
function printID(id: IDType) {
// 用typeof 实现类型缩小,将 id 从联合类型缩小为 string 类型
if(typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
printID('abc');
printID(123);
printID(true);
从上面代码可以看出,如果id 的类型是字符串(string),则调用 toUpperCase 方法。通过使用 typeof 进行类型判断,我们可以将联合类型的 id 缩小为更具体的字符串类型。
2. 平等缩小
我们可以使用switch 、 === 、== 、!== 、!= 等运算符来表达相等性,进而实现类型缩小。
示例代码:
type Dirction = 'left' | 'right' | 'top' | 'bottom';
function move(dirction: Dirction) {
// if 判断,缩小类型
if(dirction === 'left') {
console.log(dirction);
return;
} else if(dirction === 'right') {
console.log(dirction);
return;
}
// switch 判断,缩小类型
switch(dirction) {
case 'top':
console.log(dirction);
break;
default:
console.log(dirction);
break;
}
}
move('left');
move('right');
move('top');
move('bottom');
3. instanceof
JavaScript 中有一个 instanceof 运算符,用于检查一个值是否为另一个值的实例。
示例:
class Student {
studying() {
console.log('studying');
}
}
class Teacher {
teaching() {
console.log('teaching');
}
}
function work(p: Student | Teacher) {
if(p instanceof Student) {
p.studying();
} else {
p.teaching();
}
}
const stu = new Student();
work(stu);
从上面代码可以看出,在 work 函数中通过判断变量 p 是否为 Student 类型的实例,将联合类型的 p 缩小为更具体的 Student 类型。
4. in
JavaScript 中有一个 in 运算符,用于判断对象是否具有指定名称的属性。如果指定的属性存在于该对象或其原型链中,那么 in 运算符将返回 true 。
示例:
type Fish = {
swimming: () => void; // swimming是函数类型
}
type Dog = {
running: () => void; // running是函数类型
}
function walk(animal: Fish | Dog) {
// 判断 swimming 是否为 animal 对象中的属性,进行类型缩小
if('swimming' in animal) {
animal.swimming();
} else {
animal.running();
}
}
// 创建 Fish 对象,该对象的类型为 Fish 类型
const fish: Fish = {
swimming: () => {
console.log('swimming');
}
}
walk(fish);
从上面代码可以看出,在 walk 函数中通过判断 swimming 是否为 animal 对象中的属性,将 animal 的联合类型缩小为更具体的 Fish 类型。
TypeScript 函数类型详解
函数的类型
在 JavaScript 开发中,函数是重要的组成部分,并且可以作为一等公民。在使用函数的过程中,可以编写函数类型的表达式来表示函数类型。
示例代码:
// sum 函数,未编写函数类型
const sum = (a1: number, a2: number) => {
return a1 + a2;
}
// 为 add 函数编写函数类型
const add : (num1: number, num2: number) => number = (a1: number, a2: number) => {
return a1 + a2;
}
上述语法 (num1:number,num2:number)=>number 表示一个函数类型的定义。该函数类型定义了以下特征:
- 该函数接收两个参数:num1 和 num2, 它们的类型均为 number。
- 该函数返回值的类型为 number。
可以使用类型别名让函数变得更具可读性,优化后代码如下:
// 使用类型别名优化函数
type AddFnType = (num1: number, num2: number) => number;
const add: AddFnType = (a1: number, a2: number) => {
return a1 + a2;
}
console.log(add(2,3));
当函数作为另一个函数的参数时,也可以编写相应的函数类型,代码如下:
// fn 函数作为 bar 函数参数时,为 fn 函数编写类型
type FooFnType = () => void;
function bar(fn: FooFnType) {
fn();
}
bar(() => {
console.log('hello');
});
函数参数的类型
1. 可选参数
函数不仅可以接收参数,而且可以指定某些参数是可选的。
// 参数y 是可选参数, y 的类型可以为 undefined | number
function foo(x: number, y?: number) {
console.log(x,y);
}
foo(1,2); // y 为2
foo(1); // y 为undefined
上面代码, y 是可选参数,可以接收 number 或 undefined 类型的数据。如果没有向 y 传递数据,那么 y 的值就为 undefined。
2. 默认参数
从 ES6 开始, JavaScript 和 TypeScript 都支持默认参数。
function foo(x: number, y: number = 20) {
console.log(x,y);
}
foo(1,2); // y 为2
foo(1); // y 为20
可以看到,参数 y 有一个默认值20,因此 y 的类型实际上是 undefined 和 number 类型的联合。
在函数参数中,参数的顺序应该是先写必传参数,然后写有默认值的参数,最后写可选参数。
3. 剩余参数
从 ES6 开始,JavaScript 也支持剩余参数。剩余参数语法允许我们将不定数量的参数放到一个数组中。
function sum(initNum: number,...nums: number[]): number {
let total: number = initNum;
for(const num of nums) {
total += num;
}
return total;
}
console.log(sum(10,20));
console.log(sum(10,20,30));
可以看到, …nums 就是剩余参数。当调用 sum 函数时,除了第一个参数,其他的参数都会传递给 nums。
this 的类型
JavaScript 中的 this 就是表示当前函数的执行上下文,但是是在不同的情况下,它所绑定的值是不同的。
- 在一个对象中使用 this 时,它会绑定到该对象上。
- 在全局环境中使用 this 时,它会绑定到全局对象上。
在TypeScript 中 ,this 类型会被分两种情况来处理:
- TypeScript可以默认推断出 this 的类型。
- 如果需要,可以手动编写 this 的类型。
1.this 的默认推导
示例:
// this 可以被 TypeScript 推导为 info 对象
const info = {
name: 'zhangsan',
age: 18,
printName() {
console.log('this name is ',this.name);
}
}
info.printName();
述代码可以正常编译和运行。在 TypeScript 中,函数的 this 类型可以被推断出来。对于 printName 函数, TypeScript 会默认推断出其外部对象 info 作为 this 类型。
2. this 的类型不明确
在某些情况下,TypeScript 默认无法推断出 this 的类型。如果没有为 this 编写类型,那么代码将在编译时报错。
示例代码:
function dosomething(message: string) {
console.log(this.name + '在' + message );
}
const info = {
name: 'zhangsan',
age: 18,
dosomething
}
// 隐性绑定 this
info.dosomething('吃饭');
// 使用 call 函数显式绑定 this
dosomething.call({name:'lisi'},'哈哈哈');
上述代码编译时会报错。这时可以为 this 编写类型,调整代码如下:
type ThisType = {
name: string
}
function dosomething(this: ThisType, message: string) {
console.log(this.name + '在' + message );
}
const info = {
name: 'zhangsan',
age: 18,
dosomething
}
// 隐性绑定 this
info.dosomething('吃饭');
// 使用 call 函数显式绑定 this
dosomething.call({name:'lisi'},'哈哈哈');
可以看到,这里为 dosomething 函数的第一个参数添加了一个 this 参数,并将其指定为 ThisType 对象类型。这样就为 this 编写了具体的类型,再次编译并执行代码,代码可以正常运行。
函数重载
在 TypeScript 中,如果编写一个 sum 函数,希望对字符串和数字类型进行相加,可能会尝试以下的写法:
function sum(a1: number | string, a2: number | string) : number | string {
// a1 + a2 报错: Operator '+' cannot be applied to types 'string | number' and 'string | number'.
return a1 + a2;
}
上面的写法实际是错误的。可以使用以下两种方案实现函数重载。
1.使用联合类型实现函数重载
在 TypeScript 中,可以使用联合类型实现函数重载。但是,联合类型有以下两个缺点:
- a.需要进行大量的逻辑判断,以缩小类型。
- b.返回值类型仍然不确定,因此一般不推荐使用联合类型。
如下示例代码:
function sum(a1: number | string, a2: number | string) {
if(typeof a1 === 'number' && typeof a2 === 'number') {
return a1 + a2;
} else if(typeof a1 === 'string' && typeof a2 === 'string') {
return a1 + a2;
}
}
console.log(sum(1,2));
console.log(sum('zhangsan','code'));
2.使用重载签名实现函数重载
在TypeScript中,为了实现函数重载,还可以编写不同的重载签名,表示函数可以以不同的方式进行调用。通常情况下,需要编写两个及以以上的重载签名,再编写一个通用的函数。
示例代码:
function sum(a1: number, a2: number): number; // 函数重载签名
function sum(a1: string,a2: string): string;
// 通用函数体
function sum(a1: any,a2: any): any {
return a1 + a2;
}
// 调用 sum 函数
console.log(sum(1,2));
console.log(sum('zhangsan','code'));
可以看到,首先定义了两个函数的重载签名,接着编写通用的sum函数并实现相加逻辑。在调用sum 函数时,它会根据传入的参数类型决定在执行函数体时到底执行哪一个函数的重载签名。
如下示例代码,有这么一个需求,当给函数参数传入数字的时候计算传入数字的和并返回,当给函数传入的参数是字符串时,让其字符串进行相加并返回。
function add(...rest: number[]): number;
function add(...rest: string[]): string;
function add(...rest: any[]) {
let first = rest[0];
if (typeof first === 'number') {
return rest.reduce((pre, cur) => pre + cur);
}
if (typeof first === 'string') {
return rest.join('');
}
}
console.log(add(1, 2)); // 3
console.log(add('a', 'b', 'c')); // abc
标签:TypeScript,console,log,TS,number,详解,类型,const
From: https://www.cnblogs.com/moqiutao/p/18411088