我正在参加「掘金·启航计划」
TypeScript 简介
TypeScript 其实就是类型化的 JavaScript,它不仅支持 JavaScript 的所有特性,还在 JavaScript 的基础上添加了静态类型注解扩展。
从某种意义上来说,TypeScript 其实就是 JavaScript 的超集
首先理解静态类型检测概念
静态类型检测
在编译时期,静态类型的编程语言即可准确地发现类型错误,这就是静态类型检测的优势。 在编译(转译)时期,TypeScript 编译器将通过对比检测变量接收值的类型与我们显示注解的类型,从而检测类型是否存在错误。如果两个类型完全一致,显示检测通过;如果两个类型不一致,它就会抛出一个编译期错误,告知我们编码错误
一、基础类型
JavaScript 原始类型:常见的number、string、boolean、null、undefined、ES6、2020(ES11)新增原始类型symbol与bigint
Ts中对应的类型注解:
const name: string = 'str';
const age: number = 24;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 9007199254740991n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('str');
二、复杂类型
特殊类型:
undefined、null、void
null 是一个伪原始类型,它在 JavaScript 中实际上是一个对象,且所有的结构化类型都是通过 null 原型链派生而来
在 JavaScript 中,null 与 undefined 分别表示“这里有值,但是个空值”和“这里没有值”。而在 TypeScript 中,null 与 undefined 类型都是有具体意义的类型。也就是说,它们作为类型时,表示的是一个有意义的具体类型值。这两者在没有开启 strictNullChecks
检查的情况下,会被视作其他类型的子类型,比如 string 类型会被认为包含了 null 与 undefined 类型
开启严格模式下:实际上并没有太大的用处
undefined 的最大价值主要体现在接口类型上,它表示一个可缺省、未定义的属性。
而 null 的价值我认为主要体现在接口制定上,它表明对象或属性可能是空值。尤其是在前后端交互的接口,比如 Java Restful、Graphql,任何涉及查询的属性、对象都可能是 null 空对象
const userInfo: {
name: null | string
} = { name: null };
void 类型,它仅适用于表示没有返回值的函数。即如果该函数没有返回值,那它的类型就是 void。 TypeScript 的原始类型标注中也有 void,但与 JavaScript 中不同的是,这里的 void 用于描述一个内部没有 return 语句,或者没有显式 return 一个值的函数的返回值
function func1() {}
function func2() {
return;
}
function func3() {
return undefined;
}
虽然 func3 的返回值类型会被推导为 undefined,但是你仍然可以使用 void 类型进行标注,因为在类型层面 func1、func2、func3 都表示“没有返回一个有意义的值”。
可以认为 void 表示一个空类型,而 null 与 undefined 都是一个具有意义的实际类型(注意与它们在 JavaScript 中的意义区分)。而 undefined 能够被赋值给 void 类型的变量,就像在 JavaScript 中一个没有返回值的函数会默认返回一个 undefined 。null 类型也可以,但需要在关闭 strictNullChecks
配置的情况下才能成立
const voidVar1: void = undefined;
const voidVar2: void = null; // 需要关闭 strictNullChecks
any
指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式。 可以对被注解为 any 类型的变量进行任何操作,包括获取事实上并不存在的属性、方法,并且 TypeScript 还无法检测其属性是否存在、类型是否正确。
也可以把任何类型的值赋值给 any 类型的变量,也可以把 any 类型的值赋值给任意类型(除 never 以外)的变量
any 类型会在对象的调用链中进行传导,即所有 any 类型的任意属性的类型都是 any
从长远来看,使用 any 绝对是一个坏习惯。如果一个 TypeScript 应用中充满了 any,此时静态类型检测基本起不到任何作用,也就是说与直接使用 JavaScript 没有任何区别。因此,除非有充足的理由,否则我们应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。
unknown
unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量。
比如在多个 if else 条件分支场景下,它可以用来接收不同条件下类型各异的返回值的临时变量,如下代码所示:
let result: unknown;
if (x) {
result = x();
} else if (y) {
result = y();
} ...
在 3.0 以前的版本中,只有使用 any 才能满足这种动态类型场景。
与 any 不同的是,unknown 在类型上更安全。比如我们可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any
let result: unknown;
let num: number = result; // 提示 ts(2322)
let anything: any = result; // 不会提示错误
使用 unknown 后,TypeScript 会对它做类型检测。但是,如果不缩小类型(Type Narrowing),我们对 unknown 执行的任何操作都会出现如下所示错误:
let result: unknown;
result.toFixed(); // 提示 ts(2571)
而所有的类型缩小手段对 unknown 都有效,如下代码所示:
let result: unknown;
if (typeof result === 'number') {
result.toFixed(); // 此处 hover result 提示类型是 number,不会提示错误
}
never
never 表示永远不会发生值的类型,这里我们举一个实际的场景进行说明。 首先,我们定义一个统一抛出错误的函数
function ThrowError(msg: string): never {
throw Error(msg);
}
一个死循环,那么这个函数的返回值类型也是 never
function InfiniteLoop(): never {
while (true) {}
}
never 是所有类型的子类型,它可以给所有类型赋值
let Unreachable: never = 1; // ts(2322)
Unreachable = 'string'; // ts(2322)
Unreachable = true; // ts(2322)
let num: number = Unreachable; // ok
let str: string = Unreachable; // ok
let bool: boolean = Unreachable; // ok
反过来,除了 never 自身以外,其他类型(包括 any 在内的类型)都不能为 never 类型赋值
类型断言
类型断言能够显式告知类型检查程序当前这个变量的类型,可以进行类型分析地修正、类型。它其实就是一个将变量的已有类型更改为新指定类型的操作,它的基本语法是 as NewType
数组、元祖、object
三、字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:
{
let specifiedStr: 'this is string' = 'this is string';
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;
}
字符串字面量类型
可以使用一个字符串字面量类型作为变量的类型,如下代码所示:
let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'
实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合
我们使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。
type Direction = 'up' | 'down';
function move(dir: Direction) {
// ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'
数字字面量类型及布尔字面量类型
数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
}
在上述代码中,我们限定了 size 属性为字符串字面量类型 'small' | 'big',isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),margin 属性为数字字面量类型 0 | 2 | 4。
四、函数类型
在 JavaScript 中,函数是构建应用的一块基石,我们可以使用函数抽离可复用的逻辑、抽象模型、封装过程。在 TypeScript 中,虽然有类、命名空间、模块,但是函数同样是最基本、最重要的元素之一。
在 TypeScript 里,我们可以通过 function 字面量和箭头函数的形式定义函数,示例如下:
function add() {}
const add = () => {}
我们还可以显式指定函数参数和返回值的类型,示例如下。
const add = (a: number, b: number): number => {
return a + b;
}
返回值类型 在 JavaScript 中,我们知道一个函数可以没有显式 return,此时函数的返回值应该是 undefined:
function fn() {
// TODO
}
console.log(fn()); // => undefined
需要注意的是,在 TypeScript 中,如果我们显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒。
function fn(): undefined { // ts(2355) A function whose declared type is neither 'void' nor 'any' must return a value
// TODO
}
此时,正确的做法是使用 03 讲介绍的 void 类型来表示函数没有返回值的类型(这是“废柴” void 类型唯一有用的场景),示例如下:
function fn1(): void {
}
fn1().doSomething(); // ts(2339) Property 'doSomething' does not exist on type 'void'.
我们可以使用类似定义箭头函数的语法来表示函数类型的参数和返回值类型,此时=> 类型
仅仅用来定义一个函数类型而不用实现这个函数。
需要注意的是,这里的=>
与 ES6 中箭头函数的=>
有所不同。TypeScript 函数类型中的=>
用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>
是函数的实现。
如下示例中,我们定义了一个函数类型(这里我们使用了类型别名 type),并且使用箭头函数实现了这个类型。
type Adder = (a: number, b: number) => number; // TypeScript 函数类型定义
const add: Adder = (a, b) => a + b; // ES6 箭头函数
可缺省和可推断的返回值类型
函数返回值的类型可以在 TypeScript 中被推断出来,即可缺省。
函数内是一个相对独立的上下文环境,我们可以根据入参对值加工计算,并返回新的值。从类型层面看,我们也可以通过类型推断(类型推断、上下文类型推断)加工计算入参的类型,并返回新的类型,示例如下:
function computeTypes(one: string, two: number) {
const nums = [two];
const strs = [one]
return {
nums,
strs
} // 返回 { nums: number[]; strs: string[] } 的类型
}
参数类型
了解了定义函数的基本语法以及返回值类型后,我们再来详细看一下可选参数、默认参数、剩余参数的几个特性。
可选参数和默认参数
在实际工作中,我们可能经常碰到函数参数可传可不传的情况,当然 TypeScript 也支持这种函数类型表达,如下代码所示:
function log(x?: string, a = 90) {
return x + a;
}
log(); // => undefined
log('hello world'); // => hello world 90
在上述代码中,我们在类型标注的:
前添加?
表示 log 函数的参数 x 就是可缺省的。
剩余参数
在 ES6 中,JavaScript 支持函数参数的剩余参数,它可以把多个参数收集到一个变量中。同样,在TypeScript 中也支持这样的参数类型定义,如下代码所示:
function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2); // => 3
sum(1, 2, 3); // => 6
sum(1, '2'); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number'
五、其他
TypeScript的运行环境(TypeScript的编译环境):
- 通过tsc编译TypeScript到JavaScript代码;
在浏览器或者Node环境下运行JavaScript代码,通过ts-node库,为TypeScript的运行提供执行环境;
全局的安装
#安装目录
npm install typescript -g
# 查看版本
tsc --version
- 通过ts-node库,为TypeScript的运行提供执行环境
# 安装ts-node
npm install ts-node -g
# 另外ts-node需要依赖 tslib 和 @types/node 两个包
npm install tslib @types/node -g
# 可以直接通过 ts-node 来运行TypeScript的代码:
ts-node hello.ts
技术探索实践-开发中常用的配置 tsconfig.json文件
"compilerOptions": {
// 目标代码(ts -> js(es5/6/7))
"target": "esnext",
// 目标代码需要使用的模块化方案(commonjs require/module.exports/es module import/export)
"module": "esnext",
// 严格一些严格的检查(any)
"strict": true,
// 对jsx进行怎么样的处理
"jsx": "preserve",
// 辅助的导入功能
"importHelpers": true,
// 按照node的方式去解析模块 import "/index.node"
"moduleResolution": "node",
// 跳过一些库的类型检测 (axios -> 类型/ lodash -> @types/lodash / 其他的第三方)
// import { Person } from 'axios'
"skipLibCheck": true,
// export default/module.exports = {}
// es module 和 commonjs
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
// 要不要生成映射文件(ts -> js)
"sourceMap": true,
// 文件路径在解析时, 基本url
"baseUrl": ".",
// 指定具体要解析使用的类型
"types": ["webpack-env"],
// 路径解析(类似于webpack alias)
"paths": {
"@/*": ["src/*"],
"components/*": ["src/components/*"]
},
// 可以指定在项目中可以使用哪里库的类型(Proxy/Window/Document)
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
}
根据实际的项目需求合理选择Typescript,让我们一起拥抱Typescript
学习最新Typescript,请参看官网