# infer extends
infer
的时候加上 extends
来约束推导的类型,这样推导出的就不再是 unknown
了,而是约束的类型。
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
的参数 Person
是 showSkills
的参数 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
的子类型,所以报错。(可以理解为,这会导致参数类型收窄,其他地方用到的代码可能不知道已经收窄,所以会出现问题??)
返回值的位置是协变的,即赋值的函数的返回值是被赋值的函数的返回值的子类型,此处 undefined
是 void
的子类型,所以不报错。
如果是 showSkills
赋值给 showName
,因为函数声明的时候是按 Person
来约束,但是实际调用的时候是按照 Developer
类型访问属性,类型不安全,所以会报错。
可以通过配置 strictFunctionTypes
来关闭逆变的检查。
{
"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 编译流程
- 源码要先用 Scanner 进行词法分析,拆分成一个个不能细分的单词(token)
- 然后用 Parser 进行语法分析,组装成抽象语法树(Abstract Syntax Tree)AST
- 之后做语义分析,包括用 Binder 进行作用域分析,和有 Checker 做类型检查
- 如果有类型的错误,就是在 Checker 这个阶段报的
- 如果有 Transformer 插件(tsc 支持 custom transform),会在 Checker 之后调用,可以对 AST 做各种增删改
- 类型检查通过后就会用 Emmiter 把 AST 打印成目标代码,生成类型声明文件 d.ts,还有 sourcemap
# babel 编译流程
- 源码经过 Parser 做词法分析和语法分析,生成 token 和 AST
- AST 会做语义分析生成作用域信息,然后会调用 Transformer 进行 AST 的转换
- 最后会用 Generator 把 AST 打印成目标代码并生成 sourcemap
# babel 和 tsc 的区别
- 语法支持
- tsc 默认支持最新的 es 规范的语法和一些还在草案阶段的语法(比如
decorators
),想支持新语法就要升级 tsc 的版本 - babel 是通过
@babel/preset-env
按照目标环境targets
的配置自动引入需要用到的插件来支持标准语法,对于还在草案阶段的语法需要单独引入@babel/proposal-xx
的插件来支持
- tsc 默认支持最新的 es 规范的语法和一些还在草案阶段的语法(比如
从支持的语法特性上来说,babel 更多一些
如果只用标准语法,那用 tsc 或者 babel 都行,但是如果想用一些草案阶段的语法,tsc 可能很多都不支持,而 babel 却可以引入 @babel/poposal-xx
的插件来支持。
代码生成
tsc 生成的代码没有做 polyfill
的处理,想做兼容处理就需要在入口引入下 core-js
(polyfill
的实现)
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)