Skip to content
On this page

资源

1 - 简介

类型(type)是人为添加的一种编程约束和用法提示

静态类型的优点

  • 有利于代码的静态分析。不必运行代码,便可发现错误。
  • 更好的 IDE 支持,做到语法提示和自动补全。
  • 提供了代码文档。
  • 有助于代码重构。
js
// ==== 1
let obj = { message: '' };
console.log(obj.messege); // 报错  messege 拼错。 在 js 中不会报错,在 ts 报错

// ==== 2
function hello() {
  return 'hello world';
}
hello().find('hello'); // 报错 没有 find。 在 js 中不会报错,在 ts 报错

// ==== 3
let y = { foo: 1 };
delete y.foo; // 报错 在 js 中不会报错,对象的属性也是静态的,不允许随意增删
y.bar = 2; // 报错 在 js 中不会报错,对象的属性也是静态的,不允许随意增删

静态类型的缺点

  • 丧失了动态类型的代码灵活性。
  • 增加了编程工作量。
  • 更高的学习成本。
  • 引入了独立的编译步骤。将 TS 代码转成 JS 代码,才能运行。

2 - 基本用法

类型声明

js
// 变量: 类型
let foo:string;

// ====
// 参数 返回值也一样
function toString(num:number):string {
  return String(num);
}

类型推断

类型声明并不是必需的, TypeScript 会自己推断类型。

js
let foo = 123;
foo = 'hello'; // 报错 推断为 number,更改类型匹配错误

// ====
// 函数返回值也可以推断
function toString(num:number) {
  return String(num);
}

因为 TypeScript 的类型推断,所以函数返回值的类型通常是省略不写的。

TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码。由于这个原因,所有 JavaScript 代码都是合法的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。 这样设计还有一个好处,将以前的 JavaScript 项目改为 TypeScript 项目时,你可以逐步地为老代码添加类型,即使有些代码没有添加,也不会无法运行。

编译

编译:TS 转 JS 的过程

编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JS 代码,并且不会改变 JS 的运行结果。

所以,TypeScript 的类型检查只是编译时的类型检查,编译后不再检验

值与类型

“类型”是针对“值”的,可以视为是后者的一个元属性,如:3 是一个值,它的类型是 number。 TypeScript 代码只涉及类型,不涉及值。

TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。

TypeScript Playground

官网在线编译页面 | TypeScript Playground。 用于在线编译成 js 代码,学习 ts 时强烈推荐使用

tsc 编译器

官方提供的编译器: tsc

tsc 的作用就是把 .ts 脚本转变成 .js 脚本。

bash
$ npm install -g typescript

$ tsc -v
Version 5.1.6

$ tsc file1.ts file2.ts --outFile ./dist/app.js --target es2015
# 将`file1.ts`和`file2.ts`两个脚本编译成`dist`子目录下的一个 js 文件`app.js`, 转换成 es6 语法(默认es3)
# 也可以不指定 outFile 而是使用 --outDir dist 输出同名文件

编译错误处理

编译报错,tsc 命令会显示报错信息,但依然会编译生成 JavaScript 脚本。

编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了

--noEmitOnError: 一旦报错就停止编译,不生成编译产物

--noEmit: 只检查类型是否正确,不生成 JavaScript 文件。

维护成配置文件 tsconfig.json 执行 tsc 即可

json
{
  "files": ["file1.ts", "file2.ts"],
  "compilerOptions": {
    "outFile": "dist/app.js"
  }
}

ts-node 模块

非官方 npm 模块,可以直接运行 TypeScript 代码

bash
npm install -g ts-node

ts-node script.ts

# 或者 npx
# `npx`会在线调用 ts-node,从而在不安装的情况下,运行`script.ts`
npx ts-node script.ts

3 - any、unknown、never 类型

TypeScript 有两个“顶层类型”( anyunknown),但是“底层类型”只有 never 唯一一个。

any 基本定义

any 类型表示没有任何限制,可以赋予任意类型的值。

TypeScript 实际上会关闭这个变量的类型检查,由于这个原因,应该尽量避免使用any类型,否则就失去了使用 TypeScript 的意义。

any 缺点:

  • 关闭了类型校验
  • 通过赋值,污染其他具有正确类型的变量,把错误留到运行时。

any 类型主要适用以下两个场合

  1. 出于特殊原因,需要关闭某些变量的类型检查。
  2. 适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript。

any 类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),涵盖了所有下层。

any 类型推断问题

变量没有指定类型、TypeScript 会自己推断类型,如果无法推断出,TypeScript 就会认为该变量的类型是 any

TypeScript 提供 --noImplicitAny,只要推断出any类型就会报错。

bash
$ tsc --noImplicitAny app.ts

变量请务必声明类型,避免推断为 any

any 污染问题

any 因为没有类型检查,可以赋值给其他任何类型的变量,导致其他变量出错。

ts
let x:any = 'hello';
let y:number;

y = x; // 不报错

y * 123 // 不报错
y.toFixed() // 不报错

通过赋值,污染其他具有正确类型的变量,把错误留到运行时。

unknown 类型

产生原因:为了解决 any 类型“污染”其他变量的问题。

具有以下特点:

  • unknownany 含义相同:表示类型不确定,可能是任意类型。
  • 不能直接赋值给其他类型的变量,赋值给 anyunknown 以外类型的变量都会报错( 目的是避免运行时错误,因为无法确保该变量具备调用的方法或属性。)
  • 仅能少数运算符(=====!=!==||&&?!typeofinstanceof)
ts
// 与 `any` 含义相同:表示类型不确定,可能是任意类型。
let x:unknown;
x = true; // 正确
x = 42; // 正确

// 不能直接赋值给其他类型的变量,赋值给 `any` 和 `unknown` 以外类型的变量都会报错
let v:unknown = 123;
let v1:boolean = v; // 报错
let v2:unknown = 'hello';

// 直接调用`unknown`类型变量的属性和方法,或者直接当作函数执行,都会报错。
let v1:unknown = { foo: 123 };
v1.foo  // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错

// 仅能少数运算符(`==`、`===`、`!=`、`!==`、`||`、`&&`、`?`、`!`、`typeof`、`instanceof`)
let a:unknown = 1;
a + 1 // 报错
a === 1 // 正确

限制这么多, unknown 有什么用? 经过 typeof 进行 “类型缩小”, unknown 类型变量才可以使用,确保不会出错。

ts
let a:unknown = 1;

if (typeof a === 'number') {
  let r = a + 10; // 正确
}

只有明确 unknown 变量的实际类型,才允许使用它,防止像 any “污染” 其他变量。

凡是需要设为 any 类型的地方,通常都应该优先考虑设为 unknown 类型。

never 类型

“空类型”,该类型为空,不包含任何值。当然不可能存在这样的值,所以称为 never

ts
let x:never;

特点:

  • 不能赋值给它任何值,否则都会报错。
  • 可以赋值给任意其他类型。(空集是任何集合的子集,任何类型都包含了 never 类型,never 为底层类型(bottom type) )
ts
function f():never { // 报错不可能返回值,所以 never
  throw new Error('Error');
}

let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错

使用场景:

  • 类型运算之中,保证类型运算的正确性和可预测性。
  • 不可能返回值的函数,返回值的类型就可以写成 never。例如函数内 throw Error 了。
  • 联合类型,通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于 never 类型。
ts
// 联合类型分支处理
function fn(x:string|number) {
  if (typeof x === 'string') {
    // ...
  } else if (typeof x === 'number') {
    // ...
  } else {
    x; // never 类型
  }
}

never 在类型运算之中,保证类型运算的正确性和可预测性:

  • 当两个类型进行交叉类型运算(使用 & 符号)时,其中一个类型是 never 类型,结果将始终为 never 类型。
  • 当两个类型进行联合类型运算(使用 | 符号)时,其中一个类型是 never 类型,结果类型将保留自身的类型。 因此,never 类型在类型运算中起到了保证完整性的作用,防止出现意外的结果。它提醒开发者在类型运算中考虑边界情况,确保结果的正确性和可预测性。

4 - 类型系统

基本类型

ts 继承 js 8 种基本类型:

  • 原始类型(primitive value):
    • boolean
    • string
    • number
    • bigint
    • symbol
  • 复合类型:
    • object
  • 特殊值:
    • undefined
    • null

基础类型的名称都是小写字母,undefinednull 既可以为值,也可以为类型,取决于在哪使用。

复杂类型由这 8 种基础类型组成。

ts
const x:boolean = true;
const y:boolean = false;


const x:string = 'hello';
const y:string = `${x} world`;


const x:number = 123;
const y:number = 3.14;
const z:number = 0xffff;


const x:bigint = 123n;
const y:bigint = 0xffffn;
// `bigint`类型赋值为整数和小数,都会报错。
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错


const x:symbol = Symbol();


// 对象、数组、函数都属于 object 类型
const x:object = { foo: 123 };
const y:object = [1, 2, 3];
const z:object = (n:number) => n + 1;



// `undefined` 和 `null` 既可以为值,也可以为类型,取决于在哪使用。
let x:undefined = undefined;
const x:null = null;

// 如果不声明类型,赋值为`undefined`或`null`,默认推断类型为 any
// 关闭 noImplicitAny 和 strictNullChecks
let a = undefined;   // any
const b = undefined; // any
let c = null;        // any
const d = null;      // any

// 想要准确推断,使用 strictNullChecks
// tsc --strictNullChecks app.ts
let a = undefined;   // undefined
const b = undefined; // undefined
let c = null;        // null
const d = null;      // null

包装对象类型

5 种原始类型(primitive value)的值,都有对应的包装对象(wrapper object)。 包装对象:指的是这些值在需要时,会自动产生的对象。

ts
// js 中的原始类型在执行时,会自动转为包装对象
// `charAt()`方法其实是定义在包装对象上
// 这样的设计,去了将原始类型的值手动转成对象实例的麻烦。
'hello'.charAt(1) // 'e'

symbol 类型和 bigint 没有构造函数,即 new Symbol()new BigInt() 剩余的boolean、string、number 都有。

包装对象类型与字面量类型

包装对象的存在导致每一个原始类型的值都有【包装对象】和【字面量】两种情况。

ts
'hello' // 字面量
new String('hello') // 包装对象

为了区分这两种情况,TypeScript 对五种原始类型分别提供了【大写】和【小写】两种类型

  • 大写类型:同时包含包装对象和字面量两种情况
  • 小写类型:只包含字面量,不包含包装对象
ts
const s1:String = 'hello';
const s2:String = new String('hello');

const s3:string = 'hello'; 
const s4:string = new String('hello'); // 报错

建议只使用小写类型,使用包装对象的场景特别少 而且 TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。

ts
const n1:number = 1;
Math.abs(n1)

const n2:Number = 1;
Math.abs(n2) // 报错

Object 类型与 object 类型

大写的 Object 类型代表 JavaScript 语言里面的广义对象,除了 undefinednull, 囊括了几乎所有的值。

ts
let obj:Object;

// 以下都是合法的
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;


// 除了以下
obj = undefined; // 报错
obj = null; // 报错

空对象 {}Object` 类型的简写形式,平时常用空对象代替。

ts
let obj:{};
// 等同下面
let obj:Object;

无所不包的 Object 类型既不符合直觉,也不方便使用。

小写的 object 类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。

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

建议总是使用小写类型object,不使用大写类型Object

大写的 Object 类型,和小写的 object 类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。

ts
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };

o1.toString()
o1.foo // 报错

o2.toString() 
o2.foo // 报错

// 描述对象的自定义属性,在后续有解决方案,  可以搜索 【对象类型】

undefined 和 null 的特殊性

undefined 表示还没有赋值,null 表示值为空。 任何其他类型的变量都可以赋值为 undefinednull

ts
let age:number = 24;

age = null;      // 正确
age = undefined; // 正确

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

但这种设计带来了弊端

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

为了解决这个问题,可以打开 strictNullChecksundefinednull 就不能赋值给除了 any 类型和 unknown 类型之外的变量。

ts
// tsc --strictNullChecks app.ts

let age:number = 24;

age = null;      // 报错
age = undefined; // 报错

let x:undefined = null; // 报错
let y:null = undefined; // 报错

let x:any     = undefined;
let y:unknown = null;

或者配置项

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

值类型

单个值也是一种类型,称为“值类型”。

ts
let x:'hello';

x = 'hello';
x = 'world'; // 报错

const 声明的变量,如果没有注明类型,会推断该变量为值类型。

ts
// x 的类型是 "https"
const x = 'https';

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

如果赋值为对象,不会推断为值类型。因为 JavaScript 里面,const 变量赋值为对象时,属性值是可以改变的。

ts
// x 的类型是 { foo: number }
const x = { foo: 1 };

父子类型赋值关系

ts
const x:5 = 4 + 1; // 报错
// 等号左侧的类型是数值`5`,等号右侧`4 + 1`的类型,推测为`number`


let x:5 = 5;
let y:number = 4 + 1;

// 5 是 number 子类  父类型变量可以赋值子类型
y = x; 

// number 不是 5 子类   子类型变量不可以赋值父类型,毕竟子类型范围比较小
x = y; // 报错  

// 如果要让子类型变量可以赋值父类型,需要使用到类型断言
const x:5 = (4 + 1) as 5; 
// `as 5` 告诉编译器,可以把 `4 + 1` 的类型视为值类型 `5`

联合类型

联合类型(union types):多个类型组成的新类型,使用符号 | 表示。 A|B :只要属于 AB,就属于联合类型 A|B

ts
let x:string|number;

x = 123; 
x = 'abc';

联合类型可以与值类型相结合,表示一个变量的值有若干种可能

ts
let setting:true|false;

let gender:'male'|'female';

let rainbowColor:''|''|''|'绿'|''|''|'';

场景:打开了 strictNullChecks 配置后,如果某个变量确实可能包含空值,就可以采用联合类型的写法。

ts
let name:string|null;

name = 'John';
name = null;


// 多行的写法,减少心智负担
let x:
  | 'one'
  | 'two'
  | 'three'
  | 'four';

联合类型是一种“类型放大”(type widening),多类型的联合类型在使用时,需要进行“类型缩小”(type narrowing),否则报错。

ts
function printId(
  id:number|string
) {
    console.log(id.toUpperCase()); // 报错
}


// 类型缩小才不报错
function printId(
  id:number|string
) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

// 另一种类型缩小
function getPort(
  scheme: 'http'|'https'
) {
  switch (scheme) {
    case 'http':
      return 80;
    case 'https':
      return 443;
  }
}

交叉类型

交叉类型(intersection types) ,A&B:即满足 A 又满足 B

ts
let x:number&string; // never 因为不可能同时满足

交叉类型主要用于表示对象的合成

ts
let obj:
  { foo: string } &
  { bar: string };

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


// 用来为对象类型添加新属性
type A = { foo: number };
type B = A & { bar: number };
// 等同于
type B = { foo: number, bar: number };