TypeScript 类型体操 - 原理

2023-05-17 17:02:51 浏览数 (1)

# infer extends

infer 的时候加上 extends 来约束推导的类型,这样推导出的就不再是 unknown 了,而是约束的类型。

代码语言:javascript复制
type TestLast<Arr extends string[]> =
  Arr extends [...infer Rest, infer Last extends string]
    ? `Last is ${Last}`
    : 'Empty';

type TestLast1 = TestLast<['a', 'b', 'c']>; // Last is c
type TestLast2 = TestLast<[]>; // Empty

# 类型安全和型变

TypeScript 给 JavaScript 添加了一套静态类型系统,是为了保证类型安全的,也就是保证变量只能赋同类型的值,对象只能访问它有的属性、方法。遇到类型安全问题会在编译时报错。

但是这种类型安全的限制也不能太死板,有的时候需要一些变通,比如子类型是可以赋值给父类型的变量的,可以完全当成父类型来使用,也就是“型变(variant)”(类型改变)。

这种“型变”分为两种,一种是子类型可以赋值给父类型,叫做协变(covariant),一种是父类型可以赋值给子类型,叫做逆变(contravariant)。

# 协变

代码语言:javascript复制
interface Person {
  name: string;
  age: number;
}

interface Developer {
  name: string;
  age: number;
  skills: string[];
}

let person: Person = {
  name: "Cell",
  age: 18,
};

let developer: Developer = {
  name: "Cell",
  age: 18,
  skills: ["TypeScript"],
};

person = developer; // OK 协变

父子关系,在保证类型安全的基础上,增加了类型系统的灵活性。

# 逆变

代码语言:javascript复制
let showSkills: (developer: Developer) => void;

showSkills = (developer) => {
  console.log(developer.skills);
};

let showName: (person: Person) => void;

showName = (person) => {
  console.log(person.name);
};

showSkills = showName; // OK 逆变 showName 只用到了父类型属性,所以类型安全
showName = showSkills; // Error 逆变 showSkills 调用时按照子类型来访问属性,类型不安全

showName 的参数 PersonshowSkills 的参数 Developer 的父类型,在调用 showSkills 时按照 Developer 来约束类型,但实际上赋值后函数只用到了父类型 Person 的属性,所以类型安全,不会有问题。

这就是逆变,函数的参数有逆变的性质(而返回值是协变的,即子类型可以赋值给父类型)。

代码语言:javascript复制
type Func = (a: string) => void;

const func: Func = (a: "A") => undefined; // Error
// Type '(a: "A") => undefined' is not assignable to type 'Func'.
//   Types of parameters 'a' and 'a' are incompatible.
//     Type 'string' is not assignable to type '"A"'.

参数的位置是逆变的,即被赋值的函数参数要是赋值的函数参数的子类型,此处 string 不是 A 的子类型,所以报错。(可以理解为,这会导致参数类型收窄,其他地方用到的代码可能不知道已经收窄,所以会出现问题??)

返回值的位置是协变的,即赋值的函数的返回值是被赋值的函数的返回值的子类型,此处 undefinedvoid 的子类型,所以不报错。

如果是 showSkills 赋值给 showName,因为函数声明的时候是按 Person 来约束,但是实际调用的时候是按照 Developer 类型访问属性,类型不安全,所以会报错。

可以通过配置 strictFunctionTypes 来关闭逆变的检查。

代码语言:javascript复制
{
  "compilerOptions": {
    "strictFunctionTypes": false
  }
}

不过虽然可以关闭逆变的检查,但是这样就会失去类型安全的保障,所以还是建议不要关闭。

# 不变

逆变和协变都是型变,是针对父子类型而言的,非父子类型自然就不会型变,也就是不变。

非父子类型之间不会发生型变,只要类型不一样就会报错:

代码语言:javascript复制
interface Person {
  name: string;
  age: number;
}

interface Animal {
  name: string;
  taxon: string;
}

let person: Person = {
  name: "Cell",
  age: 18,
};

let cat: Animal = {
  name: "Cat",
  taxon: "Mammalia",
};

person = cat; // Error
// Property 'age' is missing in type 'Animal' but required in type 'Person'

# 类型父子关系判断

java 里面的类型都是通过 extends 继承的,如果 A extends B,那 A 就是 B 的子类型。这种叫做名义类型系统(nominal type)。

ts 里,只要结构上是一致的,那么就可以确定父子关系,这种叫做结构类型系统(structual type)。

代码语言:javascript复制
interface Person {
  name: string;
  age: number;
}

interface Developer {
  name: string;
  age: number;
  skills: string[];
}

interface Animal {
  name: string;
  age: number;
  taxon: string;
}

type IsPerson = Developer extends Person ? true : false; // true
type IsPerson2 = Animal extends Person ? true : false; // true

# 编译

# tsc 编译流程

  1. 源码要先用 Scanner 进行词法分析,拆分成一个个不能细分的单词(token)
  2. 然后用 Parser 进行语法分析,组装成抽象语法树(Abstract Syntax Tree)AST
  3. 之后做语义分析,包括用 Binder 进行作用域分析,和有 Checker 做类型检查
  4. 如果有类型的错误,就是在 Checker 这个阶段报的
  5. 如果有 Transformer 插件(tsc 支持 custom transform),会在 Checker 之后调用,可以对 AST 做各种增删改
  6. 类型检查通过后就会用 Emmiter 把 AST 打印成目标代码,生成类型声明文件 d.ts,还有 sourcemap

# babel 编译流程

  1. 源码经过 Parser 做词法分析和语法分析,生成 token 和 AST
  2. AST 会做语义分析生成作用域信息,然后会调用 Transformer 进行 AST 的转换
  3. 最后会用 Generator 把 AST 打印成目标代码并生成 sourcemap

# babel 和 tsc 的区别

  • 语法支持
    • tsc 默认支持最新的 es 规范的语法和一些还在草案阶段的语法(比如 decorators),想支持新语法就要升级 tsc 的版本
    • babel 是通过 @babel/preset-env 按照目标环境 targets 的配置自动引入需要用到的插件来支持标准语法,对于还在草案阶段的语法需要单独引入 @babel/proposal-xx 的插件来支持

从支持的语法特性上来说,babel 更多一些

如果只用标准语法,那用 tsc 或者 babel 都行,但是如果想用一些草案阶段的语法,tsc 可能很多都不支持,而 babel 却可以引入 @babel/poposal-xx 的插件来支持。

代码生成

tsc 生成的代码没有做 polyfill 的处理,想做兼容处理就需要在入口引入下 core-jspolyfill 的实现)

代码语言:javascript复制
import "core-js";

Promise.resolve;

babel 的 @babel/preset-env 可以根据 targets 的配置来自动引入需要的插件,引入需要用到的 core-js 模块

  • 引入方式可以通过 useBuiltIns 来配置
    • usage:只引入用到的 core-js 模块
    • entry:在入口引入根据 targets 过滤出的所有需要用的 core-js 模块

babel 和 tsc 生成代码的区别

tsc 生成的代码没有做 polyfill 的处理,需要全量引入 core-js,而 babel 则可以用 @babel/preset-env 根据 targets 的配置来按需引入 core-js 的部分模块,所以生成的代码体积更小。

# babel 不支持的 ts 语法

babel 不支持 const enum(会作为 enum 处理),不支持 namespace 的跨文件合并,导出非 const 的值,不支持过时的 export = import = 的模块语法。

babel 还是 tsc ?

babel 编译 ts 代码的优点是可以通过插件支持更多的语言特性,而且生成的代码是按照 targets 的配置按需引入 core-js 的,而 tsc 没做这方面的处理,只能全量引入。

而且 tsc 因为要做类型检查所以是比较慢的,而 babel 不做类型检查,编译会快很多。

可以用 tsc --noEmit 来做类型检查,加上 noEmit 选项就不会生成代码了。

# 类型检查

# 如何检查类型

源码是字符串,是没法直接处理的,会先把代码 parse 成 AST,这是计算机能理解的格式。之后的类型检查就是对 AST 结构的检查

# 如何实现类型检查

参考:babel-plugin-exercize (opens new window)

AST 可视化:astexplorer (opens new window)

0 人点赞