Skip to content

开发必备

npm: ts-node ts-node-dev

vscode: 配置vscode中Inlay Hints, 插件 errorLens、typescript-import、move-ts

变量申明空间和类型申明空间

类型申明空间不会编译到源码中,class会同时声明到变量声明空间和类型声明空间。

命令空间

单文件中如果包含import或者export语句,当前单文件会默认被命名空间包围。也可以通过namespace在一个文件中实现多个命名空间,命名空间可以嵌套。

ts
// a.ts
export namespace b {
    export const b = 13
}

export const a = 'a'

// c.ts
import { a, b } from './a.ts'
console.log(b.b)

模块

大致和esm相同,有如下注意的

  • 可以只引入类型。使用import xx = require('xx')时导入的是类型。ts3.8后使用import type { xx } from 'xx'导入类型。
  • 可以import * as xx from 'xx', 也可以export * as xx
  • 新版本对package.json处理有更新
json
{
  "main": "./dist/lib/index.js",
  "module": "./dist/es/index.js",
  "types": "dist/types/index.d.ts",
  "exports": {  // 优先
    ".": {
      "types": "dist/types/index.d.ts",
      "import": "./dist/es/index.js",
      "require": "./dist/lib/index.js"
    }
  },
}

模块解析

如果tsconfig中moduleResolution: Node 时,按照node的模块查找规则。查找规则按照path+place 的方式

path是相对或者绝对路径时,直接按照路径解析。相对路径不包含 'xx/' path是动态路径时,比如** import xx from 'xx' **。从当前文件开始查找同级目录是否存在node_modules/xx。

place规则如下

  • xx.ts
  • xx 中存在package.json 而且存在package.types所指的文件或者exports.types指定的文件
  • @types/xx
  • xx/index.ts

同时针对文件扩展名,按照.ts .tsx .d.ts 顺序解析。

另外还可以通过修改设置baseUrl和paths 映射来修改这种行为。默认情况下只要设置了baseUrl,paths会默认添加 { "": ["./"] } 配置

json
{
    "compilerOptions": {
        "baseUrl": ".",  // 相对于tsconfig.json
        "paths": {
            "*": ["*"],  // 前面一个*表示module,后面一个表示相对路baseurl的module
            "@/*": ["src/*"], // "@/xxx" => baseUrl + src + xxx
            "jquery": ["some/path/jquery"]  // "jquery" => "some/path/jquery"
        }
    }
}

如果想追踪每个module的解析情况,可以使用traceResolution: true 打印解析日志 tsc编译时会从入口文件开始,将相关的依赖文件同时编译。如果配置了noResolve选项指定了某些模块不编译,编译会报错。

rollup,webpack,tsc,nodejs对模块的处理都不同,都是对js文件处理。特别是ts还隐藏了一层类型查找的问题。

每个编译器所使用的的module解析方式不同,特别是在对package.json字段未统一。

从变量引用和类型引用两个角度分析。
变量角度

ts
// test.ts
import { a } from "a"  // 1. 寻找是否能根据baseUrl和paths匹配到模块 2.依次向上查询node_modules/a是否存在 3.如果是文件夹,先检查package.json描述,如果package.json 不存在,检查index.ts 是否存在。
import b from "a/b" // 1. 同上 2. 检查node_modules/a/b 是否存在。 3. 同上3
import c from './c' // 1. 检查是否配置了rootDirs。 2. ./c是否是文件夹,如果是参考1.3 
import d from 'd.xx' // 1. 同上1

类型角度

ts
// test.ts
import { a } from "a"  // 1. 寻找是否能根据baseUrl和paths匹配到模块,a.ts存在,直接导出a.ts的命名空间,如果只有a.js存在,需要查找a.d.ts,如果a.d.ts未查找到,同时allowJS: false,在查询是否存在当前module的 declararion 声明,查找终止。
                       // 2. 向上查找node_modules/a.d.ts,如果是文件夹,先查找package.json相关定义,再查找node_modules/a/index.d.ts。再加载同级@types/a 文件夹,按照上述步骤处理。 
import b from "a/b" // 1. 同上1 2.
import c from './c' // 1. 同上述文件夹处理方式 2. 检查是否有declaration 声明
import d from './d.xx' // 后缀当做文件名处理

*.d.ts作用

tsc 编译时通过 declaration: true 能自动生成.d.ts文件。如果某个模块还未被声明,ts会自动加载同名的x.d.ts作为声明文件。

target和module

通过module: "Commonjs", ts代码可以编译成cjs,amd,cmd和esm。但是cjs和amd中只支持导出一个对象这和ts export defalt, { xxx }行为不兼容。如果明确代码需要转换成cjs,需要按照如下方式编写

ts
import xx = require('xxx')  // 如果是export = xxx模式导出,必须使用这种默认引入
const a = 123;
export = a;

tsconfig重点字段详解

types、typeRoot和node_modules/@type

默认情况下,tsc会加载编译上下文中所有*.d.ts、同时还会加载node_modules/@type文件中所有的*.dt.ts,以及向上目录的所有node_modules/@type。通过typeRoot指定默认文件夹(不会在向上循环查找),或者通过types指定typeRoot中需要加载部分*.d.ts文件。

extends

可以继承多个配置,在需要打包不同的library时有用。不同配置文件中的配置的路径是根据该配置文件相对。

rootDir 和 rootDirs

rootDirs用于把多个不同实际位置的目录组合到一个虚拟目录中,方便引用。比如src/a.ts 和src1/b.ts,组合到一个虚拟目录中后可以import xx from './b'

rootDir设置单个的虚拟目录,用于解决打包后文件目录和src不一致问题。默认值所有非声明输入文件的最长公共路径。相当于打包前将包含的文件放置到路径.replace(rootDir, dist)。

baseurl和path

baseUrl用于处理module中non-relative,默认会在node_modules 前查找。配置paths使用可以自定义路径别名

path用于配置别名 alias: baseUrl + path。和webpack

declaration

declaration: true, 开启编译时生成.d.ts文件,默认情况下声明文件和编译后的js文件放在同一位置,通过declarationDir: 'path'指定声明文件存放路径,声明文件的

allowJS 和 checkJS

allowJS: true 允许在ts中直接在加载js文件,即使这个js文件没有.d.ts。

checkJS 加载js文件时,同样检查js的类型系统

null 和 undefined

未开启strictNullCheck时,默认是其他类型的子类型 let a: string = null;

支持具名元组, 数组化的元组

type A = [name: string, age: number, desc: string]; type B = Array<number | string>;

对元组能够使用T[number] 来获得元组所有的项组成的联合类型。

建议写不出类型时,使用以下替代

针对object使用Record<string, unknown>,数组使用unknown[]

symbol的特殊地方

symbol类型是共有的,可以添加unique 限制symbol 赋值。 let a: symbol = Symbol("1") let b: symbol = a;

const c: unique symbol = Symbol("1"); const d: unique symbol = c;

字面量类型

字面量类型总是对应原始类型(拆包类型)的子类型,函数类型没有字面量类型

联合类型和交叉类型

联合类型具有联合作用,表示几种类型的联合,使用时需要指明当前是具体是哪个类型,否则只能使用联合的部分,函数类型需要使用() 包裹起来

交叉类型具有交叉的语意表示几种类型的并集,同时具有几种类型的特性,针对基本类型一般都是never,不可能一个类型同时有boolean和number特性。如果是interface 交叉,相同属性名也是按照基本类型的规则进行交叉

类型兼容

平常说类型兼容指的某个类型赋新的类型,能否满足该类型所有特性(可以推断出如果一个类型的特性越少,越容易兼容)。类型兼容从两个角度出发,一个是根据层级,一个是根据结构。 从层级依次是 any/unknown -> null / undefined -> 原始类型封包类型 -> 原始类型的拆包类型 -> 对应类型的具体实现 -> nenver。层级最顶层的总兼容下层的类型 同一种原始类型的联合兼容对应的拆包类型,联合类型的子集的联合类型兼容对应的联合类型。

另一个角度是结构化类型兼容,如果一个类型有另一个特性的所有特性,这个类型兼容另一个类型。

枚举

ts中的枚举是双向映射的(常量枚举除外),枚举也是在变量空间和类型空间同时存在

ts
enum EnumA {
  A,
  B,
  C = 'c'
}

const enum enumConstA {
  A,
  B
}

EnumA.A 和 EnumA['c'] 都可以。 常量枚举不会再源码中编译.

函数类型

  1. 如果函数没有返回语句,建议将函数返回值标记成void, 同时不建议写行内声明比如 let a: (a: string) => number = (a: string): number => { return 1}
  2. 支持函数类型重载,比如函数的参数具有一定逻辑性时
ts
function testA(user: string, age: number):number;
function testA(user: number, age: string): string {

}
// 当第一参数传入string时,第二参数必须是number

class

抽象类和interface怎么样选择? 无脑抽象类(抽象类还是实现部分逻辑)

never

表示永远不会到达,常见于函数报错终止。在体操类型时比较常见。 string & never = never string & (string | never) = string & string | string & never = string | never = string

索引类型,工具类型

泛型是一个工具类型,用于生成其他类型 索引类型的键必须要是 keyof any => string | number | symbol。 同时索引类型中具名类型必须满足索引类型的定义定义

ts
interface A {
  propA: string;
  [k]: number // 不行,
}

使用字面量联合类型进行索引类型访问时,其结果就是将联合类型每个分支对应的类型进行访问后的结果,重新组装成联合类型。索引类型查询、索引类型访问通常会和映射类型一起搭配使用,前两者负责访问键,而映射类型在其基础上访问键值类型,我们在下面一个部分就会讲到。

注意,在未声明索引签名类型的情况下,我们不能使用 NumberRecord[string] 这种原始类型的访问方式,而只能通过键名的字面量类型来进行访问。

类型收缩

  1. 如果是直接在if语句中使用typeof,类型会收缩
  2. 如果if语句是函数判断,在函数中使用is断言
  3. 通过可辨识的属性来区分时,针对同属性名不同类型,需要具体到字面量类型
  4. instanceof 和 typeof一样,用于class类型的断言
  5. 使用asserts后,后续同作用域的代码均会变成断言的类型

interface 和 type

interface 可以实现接口合并和继承,在实现合并继承时,子接口的属性必须要要兼容父接口。

type 常用于实现联合类型、泛型、交叉类型

泛型

泛型允许默认类型type A<T = string> = T; 调用时按照type B = A。 泛型类型约束 A extends B,表示A是B的子类型。

  1. 类型自身是自身的子类型
  2. 字面量类型都是对应原始类型的子类型
  3. 联合类型子集都是联合类型的子集,
  4. 下级类型总是上级类型的子集,比如 { age: number } extends {};

泛型参数之间也存在约束,后续的参数总是能依赖前面参数进行约束, 不能后面约束前面

函数泛型

只允许出现在函数申明的地方(无法再type中定义),在类型检查时可以由函数执行时推导T的类型,同时也支持泛型约束,泛型也是总是最小字面量原则。class泛型也是同理

ts
function a<T extends string | number = string>(input: T = '123'): T{
  return input
}

extends

在泛型约束和infer中使用(理解成在申明一个变量的类型是使用),在条件语句中使用。

ts
type A<T extends string | number> = T extends `${infer K extends string} 123` ? K : never

类型中rest处理

一个数组中只能处理一个rest语句

ts
type A<T extends unknown[]> = T extends [string, number, ...string[], ...infer B extends number[]] ? B : never;
type B = A<['123', 213, '123', 13, 123]> // number[] 不能识别 '123'

分布式条件语句

ts
type A<T> = T extends 1 | 2 | 3 ? 1 : 2
type test = A<1 | 2 | 3 | 4 | 5>  // 1
type test2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 : 2 ? 2

分布式条件类型需要满足条件语句需要是一个变量(泛型或者infer)而且是不能被包,分布式语句在判断时总是把联合类型单独执行条件语句,

ts
type A = 1 | 2 | '3' | number
type b<T> = T extends string ? T : never
type test = b<A> // never | never | '3' | never

除了泛型参数使用联合类型会出现分布式条件语句外,never也会(never在作为泛型参数时会让条件语句直接返回never,但是直接调用条件语句时是最底层类型)。 any 无论何时都会出现分布式条件语句(直接作为条件参数或者泛型参数),

ts
type Tmp4<T> = T extends string ? 1 : 2;  
// 通过泛型参数传入,会跳过判断
type Tmp4Res = Tmp4<never>; // never, 不是1或者2

除了在条件中有分布式查询外,在索引查询类型,查询参数如果是联合类型,也会走分布式。

所以在涉及泛型时,一定要考虑分布式条件语句,特别是在做泛型兼容判断时

ts
type A = 'a' | 'b'
type B = {
  'a': string;
  'b': number;
  'c': 12
}
type test = B[A]  // => B['a'] | B['b'] =>  string | number

结构化类型

只要一个类型A有另一个类型B所有的特征,这个类型A就兼容类型B,object = {} {} = object

索引中新角色,创建变量 as

ts
type A<T> = { // 下面两个知识点1. 模板字符串碰到never会返回 never, 2 索引类型碰到never会跳过
  [K in keyof T as `prefix_${K & string}`] : T[K]
}

模板字符串类型

上新了,新增模板字符串,在目前只能基础类型能够匹配,同时针对联合类型有分发的功能。注意never限制

ts
type A = `${1 | 2} - ${3 | 4}` // '1 - 3' | '1 - 4' | '2 - 3' | '2 - 4'

互斥的联合类型

因为结构类型的存在,联合类型不能很好的做到互斥(复杂类型),比如下面

ts
type A = {
    age: string
}
type B = {
    name: number;
}
type C = A | B;

const test: C = {  // 无法做到只能A或者B
    age: '1';
    name: 1
}

// 改进如下
type WithOut<T, U> = {
    [K in Exclude<keyof U, keyof T>]: never; 
} &  T

ts中几个操作符

in 只能用于索引类型,对联合类型生效 as 索引类型中重命名键 keyof 生成联合类型, 常见对复杂类型包括object、元组和any(string | number | symbol)。可以取消数组的遍历,还有 P in U | keyof T 这种玩法 [] 取值,元组默认都有length 属性,对联合类型使用,会触发分布式 & 交叉类型。复杂类型交叉按照属性交叉。碰到联合类型,走数学表达式。交叉类型会组合成一个新的复杂类型 {a: string} & {b: string} 就是 {a: string; b: string}

装饰器

函数的类型兼容 协变、逆变

默认情况函数参数使用双变,返回值使用协变 配置严格模式后,函数参数使用逆变

逆变是指在判断兼容A是否兼容B,应该是执行 B extends A ,协变是是A extends B 。双变任意一个满足即可

如何判断是否是一个可以被索引的

使用keyof T extends never 判断

判断any、unknown和never

any 和任何类型交叉都是any,keyof any等于 string | number | symbol

ts
type IsAny<T> = 0 extends 1 & T ? true : false

unknown 和 任何特性联合都是unknown,除了any, keyof unknown 等于never, unknown 上级类型只能是unknown 和 any

ts
type IsUnknown<T> = unknown extends T ? IsaAny<T> extends true ? false : true : false;

never 和任何类型兼容,但是在分布式条件语句中会直接返回never,利用分布式条件的包裹性实现判断。

ts
type IsNever<T> = [T] extends [never] ? true : false

ts类型中的两种递归

  1. 使用一个累计值来获取最后结果,相当于迭代的方式, 这种迭代的方式需要将处理逻辑放在每次的结果中
ts
// 数组每一项添加一个 1
type AddOne<T extends readonly string[], R extends any[] = []> = T extends [infer First, ...infer Rest] ? AddOne<Rest, [...R, `${First} 1`]> : R
  1. 递归的方式
ts
type AddOnes<T extends readonly string[]> = T extends [infer First extends string, ...infer Rest extends string[]] ? [`${First} 1`,...AddOnes<Rest>] : T

技巧

巧用declare来实现验证

如下

ts
interface Tmp {
  name: string;
  age: number;
}
declare let tmp: Tmp
tmp.name = 123   // 用于快速验证