阅读视图

发现新文章,点击刷新页面。

用3Dmol.js展示3D分子结构

几个月前有个3D开发需求,展示化学分子结构,需求学科专业化程度较高了!

对方推荐了JSmol,但是这个跟Jmol(Java分子结构库)紧密结合。最终查找资料找到纯前端解决方案,决定用3Dmol.js实现。本文简单记录一下使用方法,免得忘了!

20250930_173221.gif

1.什么是3Dmol.js

image.png官方介绍:

3Dmol.js是一个基于WebGL的面向对象的JavaScript库,用于在线分子可视化 - 无需Java! 使用3Dmol.js,您可以将精美渲染的分子可视化添加到您的Web应用程序中。

特性

  • 支持pdb、sdf、mol2、xyz和cube格式
  • 并行分子表面计算
  • 球体、棒状、线条、十字、卡通和曲面样式
  • 基于原子属性的选择和样式设置
  • 标签
  • 与分子数据的可点击交互
  • 几何形状,包括球体和箭头

Github地址 https://github.com/3dmol/3Dmol.js

官网地址 https://3dmol.org/

2.使用3Dmol.js

2.1 安装

pnpm add 3dmol

该库基于typescript开发,友好支持typescript!

或者使用script形式引入

<script src="https://3Dmol.org/build/3Dmol-min.js"></script>

<script src="https://3Dmol.org/build/3Dmol.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/3Dmol/2.0.1/3Dmol-min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/3Dmol/2.0.1/3Dmol.js"></script>

2.1 创建场景

<div id="container" style="height:800px;width:800px"></div>
import * as $3Dmol from '3dmol/build/3Dmol.js';

const container=document.getElementById("container");
//创建视图
const viewer = createViewer(container, {backgroundColor: "orange"});

//添加绿色球体
    viewer.addSphere({center: {x: 0, y: 0, z: 0}, radius: 10.0, color: "green"});
    //缩放适配渲染内容大小
    viewer.zoomTo();
    //开始渲染
    viewer.render();
    //缩放动画  2秒缩放到0.8视角
    viewer.zoom(0.8, 2000);

效果如下

image.png

常用的几个配置

  • backgroundColor背景颜色

  • antialias是否开启抗锯齿

  • backgroundAlpha背景透明度,范围[0~1]

  • defaultcolors3D分子模型默认颜色,如使用JS模型颜色$3Dmol.elementColors.Jmol,那么3D分子模型的样式会是:

    • H氢原子白色
    • O氧原子红色
    • 元素原子都会配置对应颜色作为区别标识
  • 更多视图方法的配置可以看官网文档createViewer

常用的的方法

  • render()渲染场景内容
  • resize()调整大小,用于窗口大小变化的时候
  • spin(axis, speed)视角变化,自动绕某个轴旋转
  • clear()清空场景所有
  • setConfig(c)修改更新配置
  • setStyle()修改模型全局样式配置
  • 更多视图对象操作请看GLViewer`

2.2 添加形状并设置样式

添加球体

const sphere= viewer.addSphere({center: {x: 2.0, y: 0, z: 0}, radius: 1.0, color: "red"});

const sphere1=viewer.addSphere({center: {x: -2.0, y: 0, z: 0}, radius: 1.0, color: "red", opacity: 0.5});

参数说明

  • center中心坐标
  • radius半径大小
  • color颜色
  • opacity透明度 image.png 可以使用updateStyle修改样式
 sphere.updateStyle({color: "blue"});
 viewer.render();

注意:形状修改样式后记得执行 viewer.render才能重新渲染生效。

添加柱体

viewer.addCylinder({
      start: {x: 1.0, y: 0.0, z: 0.0},
      end: {x: -1.0, y: 0.0, z: 0.0},
      radius: 0.2,
      color: "white",
      fromCap: $3Dmol.CAP.FLAT, 
      toCap: $3Dmol.CAP.ROUND  
    });

参数说明

  • start 开始坐标

  • end结束坐标

  • radius半径大小

  • color颜色

  • fromCap开始端是否圆角,NONE无,FLAT平角, ROUND圆角

  • toCap结束端是否圆角,NONE无,FLAT平角, ROUND圆角

image.png

可以开启虚线,多段柱体

viewer.addCylinder({
      start: {x: 1.0, y: 0.0, z: 0.0},
      end: {x: -1.0, y: 0.0, z: 0.0},
      radius: 0.2,
      color: "white",
      fromCap: $3Dmol.CAP.FLAT,
      toCap: $3Dmol.CAP.FLAT,
      dashed: true,
      dashLength: 0.5,
      gapLength: 0.3
    });

参数说明

  • dashed是否开启虚线
  • dashLength段长度
  • gapLength间隔长度

image.png

添加标签

  viewer.addLabel("分子结构", {
      alignment: "center",
      font: "sans-serif",
      fontSize: 18,
      fontColor: "white",
      fontOpacity: 1,
      borderThickness: 2.0,
      borderColor: "blue",
      borderOpacity: 1,
      backgroundColor: "black",
      backgroundOpacity: 0.5,
      position: {x: 0.0, y: 0.0, z: 0.0},
      inFront: true,
      showBackground: true
    });

参数说明

  • alignment对齐方式,可选项"topLeft", "topCenter", "topRight", "centerLeft", "center", "centerRight", "bottomLeft", "bottomCenter", "bottomRight",也可以是xy坐标
  • position标签位置
  • inFront是否总在模型前面
  • font字体系列
  • fontSize字体大小
  • fontColor字体颜色
  • fontOpacity字体透明度
  • borderThickness边框宽度
  • borderColor边框颜色
  • borderOpacity边框透明度
  • showBackground是否显示背景
  • backgroundColor背景颜色
  • backgroundOpacity背景透明度

image.png

还有其他更多形状,如矩形,箭头,曲线等,可以到官方API文档查看怎么使用

https://3dmol.org/doc/GLViewer.html

2.3 加载3D分子结构模型

通过请求下载获取并添加D分子结构模型,推荐这种方法,3Dmol.js自动识别格式并解析加载,很方便。

download("/1ycr.pdb", viewer, {multimodel: true, frames: true}, (model: any) => {
      console.log("🚀 ~ model:", model);
      viewer.setStyle({}, {cartoon: {color: "spectrum"}});
      viewer.render();
    });

viewer.setStyle设置全局样式,使得3D分子结构结构以卡通模式显示

image.png

通过打印的模型信息,可以看到模型相关数据有A,B,C三条链,α,β,γ三个角的角度

image.png

设置分子结构链A为绿色

viewer.setStyle({chain: "A"}, {cartoon: {color: "green"}});

image.png

通过模型格式文本解析添加3D分子结构

这是7个水分子的3D分子结构模型文本

symmetry c1
 OpenBabel01312416543D

 21 14  0  0  0  0  0  0  0  0999 V2000
   0.59904   0.98737  -3.22086 O   0  0  0  0  0  0
  -1.51456   1.20243  -0.31112 O   0  0  0  0  0  0
   0.13045   0.14890  -1.25059 O   0  0  0  0  0  0
  -0.20952  -0.68359  -1.35644 H   0  0  0  0  0  0
  -0.69349   0.71484  -0.83445 H   0  0  0  0  0  0
  -0.05336   1.70041  -3.27063 H   0  0  0  0  0  0
   0.39550   0.49975  -2.28181 H   0  0  0  0  0  0
  -2.04082   1.71243  -0.64258 H   0  0  0  0  0  0
  -1.91286   0.87596   0.33191 H   0  0  0  0  0  0
  -0.80317  -1.78194  -1.51703 O   0  0  0  0  0  0
   4.83471   2.50607   2.80654 H   0  0  0  0  0  0
  -2.85573  -2.53790  -1.84489 O   0  0  0  0  0  0
  -0.13238  -2.61116  -1.78592 H   0  0  0  0  0  0
  -1.84863  -2.10567  -1.62688 H   0  0  0  0  0  0
  -3.18539  -2.82307  -2.85475 H   0  0  0  0  0  0
  -3.60191  -2.86990  -1.10817 H   0  0  0  0  0  0
   5.44460   1.75213   2.56206 O   0  0  0  0  0  0
   6.37580   2.09935   2.45041 H   0  0  0  0  0  0
   1.49202  -0.02736   0.38172 O   0  0  0  0  0  0
   1.15940   0.32819   1.19843 H   0  0  0  0  0  0
   0.74030   0.06841  -0.41057 H   0  0  0  0  0  0
  4  3  1  0  0  0
  5  3  1  0  0  0
  6  1  1  0  0  0
  7  1  1  0  0  0
  8  2  1  0  0  0
  9  2  1  0  0  0
 13 10  1  0  0  0
 14 10  1  0  0  0
 15 12  1  0  0  0
 16 12  1  0  0  0
 17 11  1  0  0  0
 18 17  1  0  0  0
 20 19  1  0  0  0
 21 19  1  0  0  0
M  END
$$$$

设置采用Jmol模型颜色风格,


const  viewer = $3Dmol.createViewer(container, {
      defaultcolors: $3Dmol.elementColors.Jmol,
     backgroundColor: "#f9f9f9"      
    });
    
  viewer.addModel(data, 'sdf');
  viewer.setStyle(
        {},
        {
          stick: {
            radius: 0.15
          },
          sphere: {
            scale: 0.25
          }
        }
      );
  • 其中模型格式可选'pdb', 'sdf', 'xyz', 'pqr', 'mol2',这个需要人工识别格式。非专业人士真不懂,所以还是推荐download
  • viewer.setStyle通过设置全局样式为棍棒模式

image.png

2.4 切换3D分子结构模式

有四种常见的分子展示模式

  const modes = [
    {label: "球棍模式", value: "ball"},
    {label: "线框模式", value: "line"},
    {label: "球体模式", value: "spacefill"},
    {label: "卡通模式", value: "cartoon"}
  ];

线框模式的全局样式设置

viewer.setStyle(
            {},
            {
              stick: {
                radius: 0.15
              }
            }
          );

20250930_113752.gif

球体模式的全局样式设置

viewer.setStyle(
            {},
            {
              sphere: {
                scale: 0.75
              }
            }
          );

20250930_114342.gif 球棍模式的全局样式设置

viewer.setStyle(
            {},
            {
              stick: {
                radius: 0.15
              },
              sphere: {
                scale: 0.25
              }
            }
          );

20250930_114244.gif

卡通模式的全局样式设置,注意卡通模式是特殊的,仅适用于蛋白质或核酸二级结构的可视化,如果是水分子之类则可能空白一片或者只有线。

viewer.setStyle({}, {cartoon: {color: "spectrum"}});

20250930_115437.gif

让视图内容自动沿y轴旋转

viewer.spin("y");

给模型原子添加标签

image.png

打印模型信息,可以看到在atoms属性有所有原子的信息

  • elem该原子的元素
  • xyz对应原子的位置

遍历所有原子即可添加所有元素标签

 model.atoms.forEach((item: any) => {
            viewer.addLabel(item.elem, {
            alignment:'center',
              position: {
                x: item.x,
                y: item.y,
                z: item.z
              },
              fontColor: "black",
              showBackground: false
            });
          });

image.png

当然可以通过移除所有标签和模型,添加新的模型和标签

  viewer.removeAllLabels();
 viewer.removeAllModels();

销毁视图前记得清空

viewer.clear();

3. 总结

  1. 3Dmol.js的缩放操作跟Three.js的OrbitControls有些不同,滚轮缩放是相反的,向前滚是缩小,向后滚是放大,并且找了一圈配置项,没有发现修改缩放操作的配置。
  2. 3Dmol.js的不能移动操作,只能旋转。不过可以通过viewer.setView()来配置视角位置。
  • 3D分子结构不论大小都可以通过viewer.zoomTo()来适配视角,可以通过viewer.zoom(zoom,time)设置缩放等级
  1. 3Dmol.js限制缩放视角大小的参数很鸡肋,是根据相机距离物体的距离根据判断条件,而不同分子结构的大小是不限的,有的总大小可能是100+,有的总大小只有0.5,那么限制值就得谨慎计算出合适值才行,否则就会导致显示的分子结构太小或太大。
const viewer= $3Dmol.createViewer(container,{
  lowerZoomLimit: 100,
    upperZoomLimit: 200,
 })
 
viewer.setZoomLimits(lower,upper);

通过viewer.getView()返回视角参数[pos.x, pos.y, pos.z, rotationGroup.position.z, q.x, q.y, q.z, q.w ],对应平移、缩放和旋转四元数,可以获取当前视角距离z

然后我以为通过zoom(minZoom)和zoom(maxZoom)获取对应缩放值的视角距离限制,结果发现lowerZoomLimitupperZoomLimit跟getView的z距离不是一个东西。

 viewer.zoom(0.5);
    const v = viewer.getView();
    const minZ = v[3];
    viewer.zoom(3);
    const v1 = viewer.getView();
    const maxZ = v1[3];
    viewer.setZoomLimits(minZ, maxZ);
    viewer.render();

于是,查看了一下文档,有个setViewChangeCallback方法可以监听视角变化

viewer.setViewChangeCallback((view: number[]) => {
        console.log("🚀 ~ view:", view[3]);
      });

image.png

image.png

可以看到,物体离摄像机越远,即缩小,则view的z距离值越小,物体离摄像机越近,即放大,则view的z距离值越大。

3.1 实现通用视角范围限制

第一步,遍历模型元素,获取其包围框范围

const box = {
        minx: Number.MAX_SAFE_INTEGER,
        miny: Number.MAX_SAFE_INTEGER,
        minz: Number.MAX_SAFE_INTEGER,
        maxx: Number.MIN_SAFE_INTEGER,
        maxy: Number.MIN_SAFE_INTEGER,
        maxz: Number.MIN_SAFE_INTEGER
      };
      model.atoms.forEach((item: any) => {
        box.minx = Math.min(box.minx, item.x);
        box.maxx = Math.max(box.maxx, item.x);

        box.miny = Math.min(box.miny, item.y);
        box.maxy = Math.max(box.maxy, item.y);

        box.minz = Math.min(box.minz, item.z);
        box.maxz = Math.max(box.maxz, item.z);
      });

      const size = {
        x: box.maxx - box.minx,
        y: box.maxy - box.miny,
        z: box.maxz - box.minz
      };
const distance = Math.max(size.x, size.y, size.z);

第二步,zoomTo()后获取最佳适配视角作为基础距离

const v = viewer.getView();
 const baseDistance = v[3];

第三步,根据包围框大小,设置缩放最大最小距离z,监听视角变化,基于基础距离baseDistance进行对比和限制

const minDistance = -distance * 3;
      const maxDistance = distance * 2;
      viewer.setViewChangeCallback((view: number[]) => {
        const z = view[3];
        if (z < baseDistance + minDistance) {
          //缩小限制
          view[3] = baseDistance + minDistance;
          viewer.setView(view);
        } else if (z > baseDistance + maxDistance) {
          //放大限制
          view[3] = baseDistance + maxDistance;
          viewer.setView(view);
        }
      });

20250930_182140.gif

4. Github地址

https://github.com/xiaolidan00/3dmol-project 20250930_171026.gif

参考

TypeScript 和 JavaScript 的 'use strict' 有啥不同

都叫严格模式,但它们解决的问题完全不在一个层次上

前言

写完 JavaScript 严格模式的文章,突然想到一个问题:"TypeScript 不也有个 strict: true 吗?这俩是一回事吗?开了 TS 的 strict 还要写 'use strict' 吗?"

说实话,我刚学 TypeScript 时也搞混过。看着 tsconfig.json 里的 strict: true,心想这应该和 JS 的 'use strict' 差不多吧,结果配完发现代码里还是满屏标红。

后来花了个周末把 TypeScript 编译选项挨个试了一遍,才明白:这俩虽然名字像,但压根不是一个维度的东西——一个管编译时的类型检查,一个管运行时的语言行为。

先抛几个问题,看看你是不是也有同样的困惑:

  • TypeScript 的 strict 和 JavaScript 的 'use strict' 到底啥区别?
  • 开了 TS 的 strict 模式,还需要写 'use strict' 吗?
  • 它们检查的东西一样吗?(答案是完全不一样)
  • 为啥名字这么像,却是两个东西?(这锅 TypeScript 团队真得背)

这篇文章就来聊聊,同样是"严格",它们到底严在哪里,又有什么本质区别。


目录


一个真实的困惑:我到底该开哪个?

先看一个常见场景。你在写 TypeScript 项目,tsconfig.json 里配了:

{
  "compilerOptions": {
    "strict": true
  }
}

然后在代码里写:

function greet(name) {  // TS 报错:Parameter 'name' implicitly has an 'any' type
  console.log('Hello ' + name);
}

TypeScript 立马给你标红了。你想:行,TypeScript 的严格模式生效了

但是,这时候你在文件顶部加不加 'use strict',会有区别吗?

或者反过来,如果你只写了 'use strict',没开 TypeScript 的 strict: true,又会怎样?

这就是今天要搞清楚的问题


JavaScript 严格模式回顾:运行时的守护者

先快速回顾一下 JavaScript 的严格模式(详细内容可以看上一篇文章)

它是什么?

一个运行时开关,在代码执行时改变 JavaScript 引擎的行为。

'use strict';  // 告诉 JS 引擎:"用严格模式跑这段代码"

x = 10;  // ReferenceError: x is not defined(运行时报错)

它解决什么?

JavaScript 早期设计的语言层面的问题

  • 运行时错误:把静默失败变成抛出异常
  • 危险语法:禁止容易出错的语法(比如 with、八进制字面量)
  • 意外行为:修正反直觉的行为(比如自动创建全局变量)

关键特征

mindmap
  root((JavaScript<br/>严格模式))
    运行时生效
      代码执行时检查
      依赖 JS 引擎
      无法在编译时发现问题
    语言层面
      修改语言行为
      禁止危险语法
      修正历史问题
    向后兼容
      老代码不受影响
      需要主动开启
      只影响声明的作用域

TypeScript 严格模式:编译时的守护者

TypeScript 的 strict: true 是另一个完全不同的东西。

它是什么?

一个编译选项集合,在代码编译(转换为 JS)之前进行类型检查

// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // 这是个"总开关"
  }
}

当你开启 strict: true,实际上是同时开启了这 7 个编译选项:

{
  "compilerOptions": {
    "strict": true,  // 👆 等价于下面 👇

    "noImplicitAny": true,               // 禁止隐式 any 类型
    "noImplicitThis": true,              // 禁止 this 有隐式 any 类型
    "strictNullChecks": true,            // 严格的 null/undefined 检查
    "strictFunctionTypes": true,         // 严格的函数类型检查
    "strictBindCallApply": true,         // 严格检查 bind/call/apply
    "strictPropertyInitialization": true,// 严格的类属性初始化检查
    "alwaysStrict": true,                // 始终以严格模式解析(会加 'use strict')
    "useUnknownInCatchVariables": true   // catch 变量默认为 unknown 类型
  }
}

等等,看到 alwaysStrict 了吗?这就是联系的地方!

它解决什么?

TypeScript 的严格模式解决的是类型安全问题

  • 编译时错误:在代码运行前就发现类型错误
  • 类型推断:强制明确类型,避免隐式 any
  • 空值安全:防止 null/undefined 引起的运行时错误
  • 函数安全:确保函数调用的类型正确性

关键特征

mindmap
  root((TypeScript<br/>严格模式))
    编译时生效
      转译前检查
      IDE 实时提示
      运行前发现问题
    类型系统层面
      强制类型明确
      空值安全检查
      函数类型检查
    配置灵活
      总开关
      可单独开关每个选项
          逐步迁移友好

核心差异:编译时 vs 运行时

现在重点来了,这两者的本质区别

1. 生效时机不同

flowchart LR
    A[编写代码] --> B[TypeScript 编译]
    B --> C[生成 JavaScript]
    C --> D[浏览器/Node.js 执行]

    B -.->|TypeScript strict| E[编译时检查]
    D -.->|JavaScript 'use strict'| F[运行时检查]

    style E fill:#e1f5ff
    style F fill:#fff4e1

TypeScript strict: true

  • ✅ 在编译阶段检查(你还在写代码的时候)
  • ✅ IDE 实时提示,根本不让你编译通过
  • ✅ 问题在开发阶段就被发现

JavaScript 'use strict'

  • ✅ 在运行阶段检查(代码已经在跑了)
  • ✅ 只有执行到那行代码才会报错
  • ✅ 问题可能在生产环境才暴露

2. 检查内容不同

检查项 TypeScript strict JavaScript 'use strict'
未声明变量 ❌ 不检查(这是 JS 运行时的事) ✅ 运行时报错
隐式 any 类型 ✅ 编译错误 ❌ 不检查(JS 没有类型)
null/undefined 安全 ✅ 编译错误 ❌ 不检查(运行时才知道是否为 null)
函数参数类型 ✅ 编译错误 ❌ 不检查
只读属性赋值 ✅ 编译错误(如果用了 readonly ✅ 运行时报错
重复参数名 ✅ 编译错误 ✅ 运行时报错
八进制字面量 ✅ 编译错误 ✅ 运行时报错
with 语句 ✅ 编译错误 ✅ 运行时报错
this 为 undefined ✅ 类型检查会提示 ✅ 运行时行为改变

3. 适用范围不同

TypeScript strict

  • 只在 .ts.tsx 文件中生效
  • 需要 TypeScript 编译器
  • 编译后的 JS 文件里没有类型信息

JavaScript 'use strict'

  • 在所有 JS 文件中都能用(.js.ts 编译后的文件)
  • 不需要任何工具,浏览器原生支持
  • 直接影响 JS 引擎的行为

深入对比:它们分别解决什么问题?

案例 1:未声明的变量

JavaScript 'use strict' 能捕获

'use strict';

function test() {
  myVar = 10;  // ❌ ReferenceError: myVar is not defined(运行时)
}

test();

TypeScript strict 不检查这个

// tsconfig.json: { "strict": true }

function test() {
  myVar = 10;  // ⚠️ TypeScript: Cannot find name 'myVar'
               // 但这是因为 TypeScript 要求先声明变量
               // 不是因为 strict 模式
}

TypeScript 编译后:

"use strict";  // 👈 注意这里!因为 alwaysStrict: true

function test() {
  myVar = 10;  // 运行时还是会被 'use strict' 捕获
}

结论

  • TS 的 strict 本身不处理未声明变量
  • strict 包含 alwaysStrict,会自动加 'use strict'
  • 最终还是靠 JS 的严格模式在运行时捕获

案例 2:隐式 any 类型

TypeScript strict 能捕获

// strict: true

function greet(name) {
  // ❌ 编译错误:Parameter 'name' implicitly has an 'any' type
  console.log('Hello ' + name);
}

JavaScript 'use strict' 完全不管

'use strict';

function greet(name) {
  // ✅ 没问题,JS 本来就是动态类型
  console.log('Hello ' + name);
}

结论

  • TS 的 strict 强制你明确类型
  • JS 的 'use strict' 对类型无能为力(因为 JS 没有静态类型)

案例 3:空值安全

TypeScript strict 的强项

// strict: true(包含 strictNullChecks)

function getLength(str: string) {
  return str.length;
}

const maybeStr: string | null = getSomeString();

getLength(maybeStr);
// 编译错误:Argument of type 'string | null' is not assignable to parameter of type 'string'

JavaScript 'use strict' 无能为力

'use strict';

function getLength(str) {
  return str.length;
}

const maybeStr = getSomeString();

getLength(maybeStr);
// 编译通过
// 运行时如果 maybeStr 是 null,会报错:Cannot read property 'length' of null

结论

  • TS 的 strict 在编译时就发现了潜在的 null 引用问题
  • JS 的 'use strict' 只能等到运行时才崩溃

案例 4:函数 this 类型

两者都有帮助,但方式不同

// TypeScript strict
interface User {
  name: string;
  greet(this: User): void;  // 明确 this 类型
}

const user: User = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  }
};

const greetFn = user.greet;
greetFn();
// ❌ TS 编译错误:The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'
// JavaScript 'use strict'
'use strict';

const user = {
  name: 'Alice',
  greet() {
    console.log(this.name);  // this 是 undefined
  }
};

const greetFn = user.greet;
greetFn();
// ✅ 编译通过
// 运行时报错:Cannot read property 'name' of undefined

结论

  • TS 的 strict 通过类型系统在编译时就警告你
  • JS 的 'use strict'thisundefined,在运行时才报错

实战案例:看看它们如何配合工作

完整示例:两者互补

// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // 包含 alwaysStrict: true
  }
}
// user.ts

// 1️⃣ TypeScript 的 strict 检查类型
function calculateTotal(price: number, quantity: number): number {
  // 2️⃣ TypeScript 确保参数类型正确
  if (price < 0) {
    // 3️⃣ strictNullChecks 确保不返回 undefined
    throw new Error('Price cannot be negative');
  }

  return price * quantity;
}

// 4️⃣ 编译时就发现类型错误
// calculateTotal('100', 5);  // ❌ 编译错误

// 5️⃣ 如果不小心写了未声明变量
function buggyCode() {
  totol = 100;  // ❌ TS: Cannot find name 'totol'
}

编译后的 JavaScript:

// user.js
"use strict";  // 👈 自动加上!来自 alwaysStrict: true

// TypeScript 的类型检查已经完成,这里只剩运行时代码
function calculateTotal(price, quantity) {
  if (price < 0) {
    throw new Error('Price cannot be negative');
  }
  return price * quantity;
}

// 如果 TypeScript 没拦住(比如用了 any),运行时会拦住
function buggyCode() {
  totol = 100;  // 💥 ReferenceError(被 'use strict' 捕获)
}

双重保险

  1. 第一层(编译时) :TypeScript 的 strict 检查类型、空值、函数签名
  2. 第二层(运行时) :JavaScript 的 'use strict' 检查语言层面的问题

深入理解:为什么需要两者?

JavaScript 严格模式的局限

'use strict' 再严格,也只是让错误暴露得早一点,但:

  • ❌ 不能阻止类型错误(比如把字符串传给期望数字的函数)
  • ❌ 不能保证空值安全(比如访问 null 的属性)
  • ❌ 不能检查函数签名(比如参数数量、类型)

TypeScript 严格模式的局限

strict: true 再强大,也只在编译时有效,但:

  • ❌ 不能处理动态引入的第三方库(没有类型定义的)
  • ❌ 不能检查运行时的值(比如从 API 返回的数据)
  • ❌ 如果用了 any 或类型断言,类型检查就被绕过了

两者互补

graph LR
    A[开发阶段] --> B[TypeScript strict<br/>类型检查]
    B --> C[编译]
    C --> D[运行阶段]
    D --> E[JavaScript 'use strict'<br/>语言规则检查]

    B -.-> F[捕获类型错误<br/>空值引用<br/>函数签名问题]
    E -.-> G[捕获未声明变量<br/>静默失败<br/>危险语法]

    style B fill:#e1f5ff
    style E fill:#fff4e1

最佳组合

  • TypeScript strict: true:在开发时就把大部分问题拦住
  • JavaScript 'use strict' (自动加上):作为最后一道防线,拦住 TypeScript 也管不了的运行时问题

✅ 最佳实践:该怎么配置?

1. 新 TypeScript 项目:两个都要

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,  // 👈 已经包含 alwaysStrict: true
    "target": "ES2020",
    "module": "ESNext"
  }
}

这样配置后:

  • ✅ TypeScript 会做编译时检查
  • ✅ 自动为每个文件加上 'use strict'
  • 你不需要手写 'use strict'

2. 老 TypeScript 项目:逐步迁移

如果直接开 strict: true 会导致满屏报错,可以单独开启

{
  "compilerOptions": {
    "strict": false,  // 先不开总开关

    // 逐步开启单个选项
    "noImplicitAny": true,            // 第一步:禁止隐式 any
    "alwaysStrict": true,             // 第二步:加 'use strict'
    "strictNullChecks": true,         // 第三步:空值检查
    // ... 逐步开启其他选项
  }
}

3. 纯 JavaScript 项目:只能用 'use strict'

如果你不用 TypeScript,那就只能用 JavaScript 的严格模式:

// 方式 1:全局开启(文件顶部)
'use strict';

// 你的代码...
// 方式 2:函数级别开启
function myFunction() {
  'use strict';
  // 只在这个函数内严格
}

推荐:配合 ESLint 强制添加:

// .eslintrc.js
module.exports = {
  rules: {
    'strict': ['error', 'global']
  }
};

4. 配合 ESLint/Prettier

TypeScript 的 strict 模式专注类型检查,但代码质量还需要 ESLint:

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

这样你会得到:

  • TypeScript strict:类型安全
  • ESLint:代码质量、最佳实践
  • Prettier:代码格式

对比总结表

维度 TypeScript strict: true JavaScript 'use strict'
本质 编译选项集合 运行时指令
生效时机 编译时(写代码时) 运行时(代码执行时)
检查内容 类型、空值、函数签名 语言规则、危险语法
错误提示 IDE 实时提示、编译失败 运行时抛出异常
依赖 TypeScript 编译器 JavaScript 引擎
适用文件 .ts.tsx 所有 .js 文件
性能影响 无(编译时) 微小(运行时)
向后兼容 需要 TypeScript 所有现代浏览器
配置方式 tsconfig.json 代码中写 'use strict'
关联关系 alwaysStrict 会自动加 'use strict' 无关 TypeScript
最佳实践 新项目必开 TS 项目自动加上,JS 项目手动加

常见误区澄清

误区 1:"开了 TypeScript strict 就不需要 'use strict' 了"

错误

虽然 strict: true 包含 alwaysStrict: true(会自动加 'use strict'),但:

  • TypeScript 只检查编译时的类型问题
  • 'use strict' 检查运行时的语言问题

正确理解:开了 strict: true 后,编译出的 JS 会自动带 'use strict',所以你不用手写。


误区 2:"'use strict' 能替代 TypeScript"

错误

'use strict' 再严格,也不能做类型检查。比如:

'use strict';

function add(a, b) {
  return a + b;
}

add('1', 2);  // ✅ 运行通过,结果是 '12'(字符串拼接)

TypeScript 会在编译时就发现类型问题:

function add(a: number, b: number) {
  return a + b;
}

add('1', 2);  // ❌ 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'

误区 3:"strict: true 太严格了,影响开发效率"

错误(短期看似如此,长期受益)

刚开始确实会遇到很多类型错误,但:

  • 这些错误本来就存在,只是以前被隐藏了
  • 在编译时发现远比在生产环境崩溃要好
  • 类型提示会让重构和协作更安全

建议:新项目直接开 strict: true,老项目逐步迁移。


写在最后

研究完这两个"严格模式",我的理解是:

它们的关系

  • TypeScript strict:编译时的守护者,拦截类型错误、空值引用、函数签名问题
  • JavaScript 'use strict' :运行时的守护者,拦截语言层面的危险语法和意外行为
  • 它们不是替代关系,而是互补关系

为什么要两者都用

  • TypeScript 再强大,也只在编译时有效
  • 编译后的 JS 代码,依然需要 'use strict' 在运行时提供保护
  • strict: true 里的 alwaysStrict 会自动加上 'use strict',所以你只需要配置 TypeScript,不用手写

使用建议

  1. TypeScript 项目:开启 strict: true(已包含 alwaysStrict
  2. 纯 JavaScript 项目:手动加 'use strict',配合 ESLint 强制
  3. 不要因为名字相似就混淆它们:一个管编译时类型,一个管运行时语言规则

下次有人问你"TypeScript 的 strict 和 JavaScript 的 'use strict' 有啥区别",你可以自信地说:

一个在编译时保护你的类型安全,一个在运行时保护你的代码行为。名字像,但完全不是一回事!

TypeScript 官方文档

  1. Compiler Options: strict - TypeScript 严格模式官方说明
  2. TSConfig Reference - 完整的编译选项参考

JavaScript 官方规范

  1. ECMAScript Strict Mode - 严格模式的官方定义
  2. MDN - Strict mode - 最全面的严格模式文档

相关文档

  1. TypeScript Deep Dive: Strict - 深入理解 TypeScript 严格性
  2. JavaScript: The Good Parts - Douglas Crockford 讲解严格模式的设计哲学

TS 模板字符串类型:从基础到进阶的类型编程魔法

TS 模板字符串类型:从基础到进阶的类型编程魔法

一、快速上手:TS 模板字符串类型初体验

TS 模板字符串类型是类型系统层面的「字符串拼接魔法」,语法与 JS 模板字符串相似,但核心区别在于:JS 用于运行时字符串拼接,TS 用于编译时生成字符串字面量类型

// JS模板字符串(运行时)

const name = "Alice";

const msg = `Hello, ${name}`; // 运行时生成"Hello, Alice"

// TS模板字符串类型(类型层面)

type Greeting<T extends string> = `Hello, ${T}`;

type Msg = Greeting<"Bob">; // 类型推导为"Hello, Bob"

它通过 ${} 插入类型变量,支持字符串字面量类型(如 "name")、联合类型(如 "a" | "b")、内置工具类型(如 Uppercase<T>),示例:

type Prefix<T extends string> = `prefix_${T}`;

type UserID = Prefix<"123">; // "prefix_123"

type Event<T extends string> = `on${Capitalize<T>}Click`;

type ClickEvent = Event<"double">; // "onDoubleClick"

二、核心特性解析:解锁类型编程新姿势

联合类型的「批量拼接」

模板字符串类型作用于联合类型时,会生成所有可能组合,实现「类型级联」:

type Properties = "name" | "age" | "email";

type Getter<T extends string> = `get${Capitalize<T>}`;

type Getters = Getter<Properties>; // 推导为"getName" | "getAge" | "getEmail"

条件类型与模板字符串的「逻辑组合」

结合 extends 可实现动态前缀 / 后缀添加或类型过滤,仅处理符合条件的类型:

// 为http开头的字符串添加协议后缀,否则设为never

type AddProtocol<T extends string> = T extends `http${string}`
    ? `${T}://api`
    : never;

type ValidURL = AddProtocol<"https">; // "https://api"

type InvalidURL = AddProtocol<"ftp">; // never(被过滤)

嵌套模板:构建复杂类型结构

通过多层嵌套生成层级化字符串类型,典型场景为 API 路径建模:

type BasePath = "/api";

type Version = "v1" | "v2";

type Resource = "users" | "posts";

type ApiPath = `${BasePath}/${Version}/${Resource}`;

// 推导为"/api/v1/users" | "/api/v1/posts" | "/api/v2/users" | "/api/v2/posts"

三、实战应用:让类型安全贯穿开发全流程

API 数据建模:告别「魔法字符串」

用模板字符串类型定义 API 路径与请求方法,实现编译时路径校验:

type Method = "GET" | "POST" | "PUT";

type Endpoint = "users" | "orders";

type ApiConfig<M extends Method, E extends Endpoint> = {
    url: `${E}/:id`; // 路径参数自动校验
    method: M;
};

// 合法:url为"users/:id",method为"GET"

const config: ApiConfig<"GET", "users"> = { url: "users/:id", method: "GET" };

状态管理:Zustand + 模板字符串的类型安全实践

Zustand 结合模板字符串可自动生成状态更新 Action 类型,避免手动维护字符串常量:

import { create } from "zustand";

// 1. 定义状态字段与Action类型

type AppStateKey = "theme" | "language";

type AppActionType = `UPDATE_${Capitalize<AppStateKey>}`; // 推导为"UPDATE_Theme" | "UPDATE_Language"

// 2. 定义状态更新参数类型

type UpdatePayload = {
    [K in AppStateKey]: K extends "theme"
        ? "light" | "dark"
        : "zh-CN" | "en-US"
};

// 3. 创建类型安全的Store

const useAppStore = create<{
    theme: UpdatePayload["theme"];
    language: UpdatePayload["language"];
    dispatch: <T extends AppStateKey>(
       action: `UPDATE_${Capitalize<T>}`,
       payload: UpdatePayload[T]
    ) => void;
}>((set) => ({
     theme: "light",
     language: "zh-CN",
     dispatch: (action, payload) => {
       switch (action) {
         case "UPDATE_Theme": set({ theme: payload }); break;
         case "UPDATE_Language": set({ language: payload }); break;
       }
     },
}));

// 4. 类型安全保障:错误用法直接编译报错

useAppStore.getState().dispatch("UPDATE_Theme", "dark"); // ✅ 合法
useAppStore.getState().dispatch("UPDATE_User", "admin"); // ❌ 非法Action
useAppStore.getState().dispatch("UPDATE_Theme", "red"); // ❌ 非法payload

对象键名重映射:动态生成规范键名

通过映射类型 + 模板字符串批量修改对象键名(如添加前缀):

type AddPrefix<T, Prefix extends string> = {
     [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];
};

interface User { name: string; age: number; }

type PrefixedUser = AddPrefix<User, "user">; // 推导为{ userName: string; userAge: number }

四、高级技巧:玩转类型级字符串操作

infer + 模板字符串:实现类型级字符串解析

利用 infer 提取字符串片段,实现类型级 Trim 等操作:

// 移除字符串左侧空格(类型级TrimLeft)

type TrimLeft<Str extends string> = Str extends `${infer Rest}`;
     ? TrimLeft<Rest>
     : Str;

type Trimmed = TrimLeft<"  Hello World">; // 推导为"Hello World"

递归模板:处理复杂字符串模式

通过递归调用实现字符串反转等复杂逻辑:

// 反转字符串(递归实现)

type ReverseString<Str extends string> = Str extends `${infer First}${infer Rest}`
    ? `${ReverseString<Rest>}${First}`
    : Str;

type Reversed = ReverseString<"abcdef">; // 推导为"fedcba"

五、避坑指南:用好模板字符串类型的关键

  1. 避免过度复杂的联合类型:成员过多(如超 100 个)会导致编译变慢,建议拆分或用 Extract/Exclude 筛选。

  2. 注意类型推断边界:仅支持字符串字面量类型,无法处理运行时动态字符串(如 let str: string = "name"),需用 as const 转为字面量类型。

let dynamicStr: string = "name";

type DynamicType = `get${Capitalize<dynamicStr>}`; // ❌ 报错
  1. 合理结合内置工具类型:善用 Uppercase/Lowercase/Capitalize,避免重复造轮子。

六、总结:模板字符串类型的核心价值

TS 模板字符串类型是「类型系统」与「字符串操作」的桥梁,核心价值在于:

  1. 类型安全:编译时校验字符串格式,杜绝运行时「魔法字符串」错误;

  2. 高效开发:通过类型推导减少手动编码,提升 IDE 智能提示;

  3. 逻辑复用:构建可复用的类型逻辑,降低维护成本。

从基础拼接至复杂递归操作,它让 TS 类型系统具备「可编程性」,是写出健壮代码、解锁类型编程潜力的关键特性。

史上最全typescript使用指南

一、ts开发环境搭建

在node环境中搭建typescript,使用npm指令安装

npm install -g typescript

然后使用tsc就可以使用编译ts文件

1.1 ts默认假设(设置)

默认情况下,ts会做出下面几种假设:

  • 假设当前的执行环境是浏览器环境
  • 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行

举个例子

新建一个index.ts文件

let test:string = "123"

执行tsc index.ts,会生成一个index.js文件

然后index.ts文件会报错,test变量不能被重新声明,原因是因为,index.js文件中也有test变量,由于ts默认是全局执行,所以在index.ts中的test变量就不能被重新声明了

  • 编辑的目标代码是es3(最大的兼容性)

但是,我们可以手动的改变这些假设

1.2 改变ts默认设置(假设)

  • 使用ts命令行时加上参数(不推荐)
  • 使用ts配置文件,更改编译选项

二、ts配置文件

使用配置文件后,使用tsc编译时不能更上文件名了,跟上文件名就会忽略配置文件

新建ts配置有两种方式

  • 在根目录下手动新建tsconfig.json文件
  • 使用命令行的方式tsc --init生成tsconfig.json文件

tsconfig.json配置文件有许多的配置

2.1 compilerOptions配置编译选项

{
    "compilerOptions":{
            "target":"es2016",                  //js代码标准,默认为es3
            "module":"commonjs",                 //编译目标(编译后的js代码)使用的模块化标准,一般为commonjs(node环境),es6(浏览器环境)
            "lib":["es2016"],                    //ts运行的全局环境,比如es2016代表ts可以使用es2016这个库的环境,但是没有浏览器环境了,像console、document等这些都不再支持了,要解决这个问题就需要安装一个库@types/node。@types是ts官方的类型库,其中包含了很多对js代码的描述,像很多三方库没有ts类型描述,但是又需要类型检测,所有可以去@types中检测是否有。
            "outDir":"./dist"                    //编译后的输出文件目录
    },
    "include":["./src"],                                 //需要编译的文件
    "files":["./src/xx"]                                 //指定编译文件及其依赖文件
}

三、使用三方库简化流程

通过上面的案例知道,ts编译运行需要先执行编译命令,然后再使用node运行文件,并且同时文件变更后有需要重新编译运行,这个相当麻烦,因此有一些三方库来帮助我们简化这些流程。

3.1 ts-node

将ts代码在内存中完成编译,并且完成运行

npm install -g ts-node             //全局安装

运行命令

    ts-node 入口文件                 //注意这种方式不会输出编译后文件

3.2 nodemon

用于检测文件变化

全局安装

npm install -g nodemon 
nodemon --exec ts-node src/index.ts     //该命令的作用是,当文件发生变化就执行后面的命令

添加配置,只要监控ts文件

-e 后面跟需要监控的文件类型

nodemon -e ts --exec ts-node src/index.ts     

--watch 后面跟监控的文件夹

nodemon --watch src -e ts --exec ts-node src/index.ts

注意tsc有一个命令

tsc --watch

这个命令只会监听编译,但是不会执行编译文件,只要有文件变更就会出编译结果。

四、ts中使用模块化

4.1 新增的tsconfig.json配置

配置名称 含义
module 设置编译结果中使用的模块化标准
moduleResolution 设置解析模块的模式
noImplicitUseStrict 编译结果中不包含"use strict"
removeComments 编译结果移除注释
noEmitOnError 错误时不生成编译结果
esModuleInterop 启用es模块化交互非es模块化导出
experimentalDecorators 开启装饰器语法
noImplicitAny 开启隐式any检查
declaration 编译结果是否生成声明文件

4.2 回顾前端模块化标准

参考文章juejin.cn/post/749299…

4.2.1 ts中如何书写模块化语句?

在es6出来之后,ts就已经完全支持es标准,因此ts中导入导出统一使用es6的模块化标准即可。

4.2.2 编译结果是怎样的?

可在tsconfig.json文件里配置

{
    "compilerOptions":{
            "module":"commonjs"             //配置编译目标使用的版本,commonjs、es6...
    },
}

注意

  • 如果编译结果的模块化标准是es6,则编译结果没有区别
  • 如果编译结果是commonjs,导出的声明会变成exports的属性,默认导出会变成exports的default属性

4.3 模块解析

4.3.1 什么是模块解析?

应该从什么位置寻找和解析模块?

4.3.2 模块解析策略

  • classic:经典模块解析策略(es6之前就已经出现,已经过时)
  • node:node解析策略(后面写node文章补充上)

推荐设置配置moduleResolution:"node"使用node解析

4.4 默认导出模块报错

在ts文件中直接书用fs这种默认导出模块会报错,比如

import fs from "fs" 
fs.readFileSync("./")  //报错

//编译后文件
const fs_1 = require("fs")
    fs_1.default.readFileSync("./")  编译错了,因为fs模块不是用ts写的,fs这个模块使用的module.exports = {}

解决办法

import {readFileSync} from "fs" 
readFileSync("./")  //不再报错

//编译后文件
const fs_1 = require("fs")
    fs_1.readFileSync("./") 

或者

import * as fs from "fs"
fs.readFileSync("./")  //不再报错

//编译后文件
const fs_1 = require("fs")
    fs_1.readFileSync("./") 

或者

//1、添加配置 esModuleInterop:true
import fs from "fs"
fs.readFileSync("./")  //不再报错

//编译后文件
会生成辅助函数,可自行查看编译结果

小结,即当在开发过程中如果遇到三方库使用的是module.export = {} 方式导出的,则使用上述办法解决

4.5 如何在ts中书写commonjs模块化代码

4.5.1 使用commonjs规范书写

//a.js文件

module.exports = {
    name:"test"
}

//b.js文件
const myModule= require("./a")    //myModule 类型是any

上面代码会出现问题,类型检查没了,因此ts提供一种语法解决这个问题

4.5.2 ts中的语法

//a.js文件
export = {
    name:"test"
}

//第一种语法,推荐
//b.js文件导入
//使用es6模块化导入,需要开启esModuleInterop为true
import myModule from "./a"        //myModule 类型是{name:string}

//第二种语法
//b.js文件导入
import myModule= require("./a")    //myModule 类型是{name:string}

更多的模块化相关的配置可以参考上面

五、ts基本数据类型

5.1 基础数据类型

  • number

  • string

  • boolean

  • array

    约束数组类型必须约束数组每一项item的类型

    比如数字类型数组 number[],字符串数组 string[]等等

    也可以这样鞋 Array<number>,同等效果

  • object

    这个对象只能约束对象,不能约束对象内部具体的值

  • null 和undefined

    这两种类型是其他类型的子类型,它们可以赋值给其他类型

    比如

    let str:string = undefined;
    str.toUpperCase();                  //会报错
    

    为了避免上面情况发生,可以在tsconfig.json中配置ts的严格模式

    {
        "compilerOptions":{
            "strictNullChecks":true       //更加严格的空类型检查
        }
    },
    

    加上strictNullChecks后undefined和null就不能赋值给其他类型了,只能赋值给自身。

5.2 其他常用类型

  • 联合类型

就是一个变量既可以是一种类型又可以是另一种类型,比如

let str:string|undefined;

多种类型任选其一,可以配合类型保护进行判断

类型保护:当对某个变量进行类型判断之后,在判断语句块中便可以确定它的确切类型,typeof可以触发类型保护

  • void类型

约束函数的返回值,函数不返回任何值

  • never类型

通常约束函数的返回值,表示函数永远不可能结束

比如抛出错误

function throwError(msg:string):never{
    throw new Error(msg)
}

function alwaysDoSomething():never{
    whild(true){
    }
}
  • 字面量类型 明确约束变量的值

比如

let age:"18"|"20"; //age 只能取值 "18""20"

let arr:[]        //这也是字面量约束,表示永远只能取空数组

let obj:{}       //这也是字面量约束,表示永远只能取空对象
  • 元组类型

表示固定长度的数组,并且数组每一个项的类型都确定

  • any类型

绕过类型检查,所以any可以赋值给任意类型

六、扩展类型

6.1 字面量类型的问题

问题一:字面量在做约束时会产生很多的重复代码(可使用使用类型别名解决)

比如

    let gender:"男"|"女"
    gender = "男"
    
    function searchUser(g:"男"|"女"){}
  • 类型别名

    对已知的一些类型定义名称

    type 类型名称 = 具体类型
    
    type Gender ="男"|"女"
    let gender:Gender="男"
     function searchUser(g:Gender){}
    //定义了一个User类型
    

问题二:逻辑名称和真实的值产生了混淆,会导致修改真实值的时候产生大量的修改。

问题三:字面量类型不会进入到编译结果

比如

   let gender:"男"|"女"
    gender = "男"                      //想象一下现在取值的地方有很多很多地方
    
    function searchUser(g:"男"|"女"){}

当有一天需要把男变成帅哥,把女变成美女,那改动就大了去了。这就是所谓了把真实值和逻辑值混淆了。使用枚举可以解决。

6.2 枚举

枚举通常用来约束变量的取值范围。

6.2.1 枚举定义语法

enum 枚举名 {
    枚举字段1 = 值1,
    枚举字段2 = 值2,
    ...
}

上面的问题可以如下解决

   enum Gender {
       male = "男",
       female = "女"
   }
    gender = Gender.male                    //想象一下现在取值的地方有很多很多地方,也没关系了

枚举会参与编译,并且会出现在编译结果中即出现在js中,表现为对象。

6.2.2 枚举的规则

  • 枚举的值可以是数字和字符串

    数字枚举有一些特点,数字枚举的值会自动自增,比如

    enum Number {
        one,
        two,
        ...     //one 的值为 1会自增, ....
    }
    

    被数字枚举约束的值,可以直接赋值给变量,一般不要这么做

    enum Number {
        one=1
        two,
        ...     //one 的值为 1会自增, ....
    }
    let one:Number = 1    //等于 let let one:Number = Number.one
    one=2
    

    数字枚举和字符串枚举的编译结果有差异

    以上面为例子,编译后会出现类似这样的代码

         {
             one:1,
             two:2,
             1:"one",
             2:"two"
         }
    

注意

  • 尽量不要在一个枚举中既出现数字又出现字符串
  • 使用枚举值,尽量使用枚举字段名称,不要直接使用真实值

6.3 接口与类型兼容

什么是接口?

用于约束类、对象、函数的标准

注意接口不会出现在编译结果中即不会出现在编译后的js文件中。

6.3.1 约束对象

语法

interface 名称 {

}

这个看起来和类型别名没啥区别,确实在约束对象上它们俩没啥区别,最大的区别在约束类上。建议约束对象使用接口。

6.3.2 约束函数

interface Test {
    (n:number):boolean
}
//等于

type Test = (n:number)=>boolean

//等于
type Test ={
     (n:number):boolean
}

6.3.3 接口继承

使用extends关键字实现

interface A {
    T1:string
}
interface B extends A {

}          //B中有T1

interface C extends B,A {}    //可以继承多个

使用type也可以实现类似效果使用&符号,交叉类型,但是更加推荐使用接口的继承。

type的交叉类型和interface的继承有比较大的区别

interface继承不能重新覆盖父interface的成员,而交叉类型是可以的,但是交叉类型会合并相同成员类型交叉

补充 readonly修饰符

只读修饰符

interface User {
   readonly id:string
}

const user:User ={
    id:"123"
}
user.id="234" //编译不能通过,因为这个变量为只读形式

只读修饰符也不在编译结果中

6.3.4 类型兼容性

ts中判断类型是否兼容(变量能够被赋值),使用的子结构辩型法:目标类型需要某一些特征,赋值的类型只要满足该特征即可。

  • 基本类型判断:完全匹配
  • 字面量类型判断:完全匹配
  • 对象类型:子结构辩型法

比如

interface Parent {
    bloodType: "A",
    feature:"圆脸"
}

const child = {
    bloodType:"A",
    feature:"圆脸",
    age:18,
    swimming:true
}

const parent:Parent = child    //赋值成功,因为child有parent的特征

但是如果直接赋值ts会报错,因为使用字面量赋值的时候,ts会进行严格判断

interface Parent {
    bloodType: "A",
    feature:"圆脸"
}


const parent:Parent =  {
    bloodType:"A",
    feature:"圆脸",
    age:18,
    swimming:true
}      //赋值失败,因为ts会进行严格判断

  • 函数类型:

    参数处理:传递给目标函数的参数可以少不能多

    返回值处理:返回值类型一定要匹配,要求返回必须返回,如果不要求返回,则随意

6.4 类

在ts中的属性类需要使用属性列表、比如

class User {
    constructor(name:string,age:number){
        this.name = name;              //会报错,因为缺少属性列表描述
        this.age = age;                //会报错,因为缺少属性列表描述
    }
}

正确写法

class User {
    name:string
    age:number
    constructor(name:string,age:number){
        this.name = name;              
        this.age = age;                
    }
}

有时可能会写出这种很奇怪的代码

class User {
    name:string
    age:number
}

这个User类有name和age的属性列表描述,但是在User中有没有地方使用即这个name和age没有进行初始化,值为undefined。可以通过在tsconfig.json中新增strictPropertyInitialization:true配置,检查是否初始化

6.4.1 设置class属性列表默认值

class User {
    name:string = "zhangsan"
    age:number = 18
    constructor(name:string,age:number){
    }
}

由上可看,可以直接在属性描述列表中赋初值。

6.4.2 属性赋值修饰符

readonly,表示当前属性为只读

class User {
    readonly name:string = "zhangsan"
    age:number = 18
    constructor(name:string,age:number){
    }
}

?,表示当前属性为可选

class User {
    name?:string
    age:number = 18
    constructor(name:string,age:number){
    }
}

6.4.3 访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,代表所有代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:受保护的成员,只能在自身和子类中访问

上面这些修饰符只会存在ts中,编译后的代码是没有的。

ts提供了一种语法糖,当构造函数的参数传入进来被直接赋值的时候,可以使用一种特殊的语法糖

class User {
    name:string
    age:number
    constructor(name:string,age:number){
        this.name = name;
        this.age = age;
    }
}

等于

class User {
    constructor(public name:string,public age:number){
    }
}
//注意一定要加访问修饰符

6.4.4 访问器

作用

控制访问属性的读取和赋值

这里与es2016的语法一样 设置set和get

参考文章 juejin.cn/post/755391…

6.4.5 class作为实例类型的约束

在 TypeScript 中,类(class)可以直接作为类型约束,用于约束变量、泛型参数或函数参数的类型:

  • 类作为实例类型的约束

    类本身代表其实例的类型,因此可以直接用类名约束变量必须是该类的实例。

    class Person {
      name: string;
      constructor(name: string) {
      this.name = name;
      }
   }

    // 约束变量必须是 Person 实例
    function greet(p: Person) {
      console.log(`Hello, ${p.name}`);
    }

    const person = new Person("Alice");
    greet(person); // ✅ 合法
    greet({ name: "Bob" }); // ❌ 报错:缺少 Person 类的构造逻辑
  • 类作为泛型约束

    在泛型中,可以通过 T extends Class 的形式约束泛型参数必须是类的实例类型。

class Animal {
  makeSound() {
    console.log("Some sound");
  }
}

function createInstance<T extends Animal>(ctor: new () => T): T {
  return new ctor();
}

class Dog extends Animal {
  makeSound() {
    console.log("Woof!");
  }
}

const dog = createInstance(Dog); // ✅ 返回 Dog 实例
dog.makeSound(); // 输出 "Woof!"

  • 类的构造函数类型约束

    如果需要约束泛型参数是类的构造函数(而非实例),可以结合 new () => T 使用:

function factory<T>(ctor: new () => T): T {
  return new ctor();
}

class User {
  id = Math.random();
}

const user = factory(User); // ✅ 返回 User 实例

6.5 泛型

谈起泛型先看一个典型的函数场景

function handleArray (arr:any[],n:number):any[] {
    if(n >= arr.length) return arr;
    const new Arr:any[] = [];
    for(let i = 0; i<n; i++){
        newArr.push(arr[i]);
    }
    return newArr
}
const newArr = handleArray([2,3,4,5],2)

上面这个操作数组的案例中,由于在声明handleArray处理方法时不知道用户会出进来怎样的数组,因此只能宽泛约束数组,如果用户传入的是一个字符串数组,也应该进行处理。总而言之就是对于数组的约束力度仍然是不够了。为了更加精确的约束,就需要范型了

泛型定义:是指附属于函数、类、接口、类型别名之上的类型,泛型相当于一个类型变量,只有当调用时才能确定类型

6.5.1 函数中使用泛型

在函数名之后写上 <泛型名称>

上述例子优化

function handleArray<T> (arr:T[],n:number):T[] {
    if(n >= arr.length) return arr;
    const new Arr:T[] = [];
    for(let i = 0; i<n; i++){
        newArr.push(arr[i]);
    }
    return newArr
}
const newArr = handleArray<number>([2,3,4,5],2)

按照上面的语法处理,handleArray在调用的时候就确定了要处理的数组类型了

同时在实际开发中当你使用泛型之后,ts也会很智能的推导数据类型

比如

const newArr = handleArray([2,3,4,5],2)
//不用传入number类型,ts会自动推导出数据类型

6.5.2 泛型默认值

可以在写泛型的地方这样写赋默认值

function handleArray<T = number> (arr:T[],n:number):T[] {
    if(n >= arr.length) return arr;
    const new Arr:T[] = [];
    for(let i = 0; i<n; i++){
        newArr.push(arr[i]);
    }
    return newArr
}

6.5.3 泛型在type、interface、class中的使用

直接在type、interface、class后写上泛型

type test1<T> = (n:T,i:T) => boolean

interface test2<T> = {
    name:T
}

class Test3<T> {
    name:T
}

6.5.4 泛型约束

使用extends约束、比如下面案例

interface test1 {
    name:string
}

function test2<T = extends test1> (value:T):T{
}
//value中一定有name属性

6.5.5 多范型

很简单就是传入多个范型值

interface test1 {
    name:string
}

function test2<T = extends test1,K> (value:T,value2:K):T|K{
}

6.6 class的继承

class的继承主要是描述类之间的描述关系,同时解决开发中的重复代码。具体语法和es6一致

参考文章 juejin.cn/post/755391…

子类可以重写父类

  • 子类成员不能改变父类成员的类型

    class Test1 {
        name = "test"
    }
    
    class Test2 extends Test1 {
        name = 123        // 错误,必须是字符串类型
        
    }
    
  • 无论属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配

    class Test1 {
        name = "test"
        testfunction(){
            console.log("test1function")
        }
    }
    
    class Test2 extends Test1 {
        testfunction(){
            console.log("test2function")
        }
    }
    
    const obj = new Test2()
     obj.testfunction()  //test2function
    
  • 注意this的关键字,在继承关系中,this的指向是动态的,调用方法时,根据具体的调用者确定this指向

    class Test1 {
        name = "test"
        testfunction(){
            console.log(`${this.name}`)
        }
    }
    
    class Test2 extends Test1 {
        name = 123
        testfunction(){
            console.log(`${this.name}`)
        }
    }
    
    const obj = new Test2()
     obj.testfunction()          //123

类型匹配

子结构变形法

  • 子类对象始终可以赋值给父类

单根性和传递性

单根性:每个类最多只能拥有一个父类 传递性:如果A是B的父类,并且B是C的父类,则可以认为A也是C的父类

6.7 抽象类

为什么需要抽象类? 以大家周知的中国象棋为例,可以根据中国象棋的棋子创建如下对象

    class Chess {}    //基础棋子对象
    class Horse extends Chess {}  //马
    class Pao extends Chess {}    //炮
    
    const horse = new Horse()
    const Pao = new Pao()

当代码写多了之后可能会出现以下情况

    class Chess {}    //基础棋子对象
    class Horse extends Chess {}  //马
    class Pao extends Chess {}    //炮
    
    const horse = new Horse()
    const Pao = new Pao()
    ....
    const chess = new Chess()  //语法没错,但是从面向对象的理解来说是不对的,因为中国象棋中并没有棋子这样的棋子,只有车、马、炮、兵等

为解决上述问题,抽象类诞生。语法,在Chess前面加上 abstract

    abstract class Chess {}    //基础棋子对象,加上abstract之后表示一个抽象概率,只作为提供子类公共代码,并没有实际意义
    class Horse extends Chess {}  //马
    class Pao extends Chess {}    //炮
    
    const horse = new Horse()
    const Pao = new Pao()
    ....
    const chess = new Chess()  //语法报错,抽象类无法创建实类

有时,某个类只表示一个抽象概率,主要用于提取子类共有成员,而不能直接传健它的对象。该类可以作为抽象类。给类前面加上abstract,表示该类是一个抽象类,不可创建抽象类对象。

6.7.1 抽象类成员

抽象类中可以有抽象成员,抽象成员只能出现在抽象类中

  • 父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或者实现是什么,因此需要一种强约束,让继承该类的子类必须要实现该成员。

    abstract class Chess {
        abstract name:string           //抽象属性
        abstract getName:()=> void.    //抽象方法
    }
    class Horse extends Chess {
        name = "Horse"              //不重写会报错,因为name为抽象成员,必须复写实现
        getName(){
            console.log("getName")
        }
    }
    

6.8 再学接口(interface)

在上面对接口进行了一次定义,接口用于约束类、对象、函数是一个类型契约。当时并没有去约束类,同时接口与类型别名最大的区别就是接口可以和类进行联用

以一个案例来体会接口与类的联用

有一个马戏团,马戏团中有很多动物;包括:狮子、老虎、猴子、狗,这些动物都有共同的特别,拥有名字,年龄、种类名称。还有一个共同的方法:打招呼,它们各自有各自的技能,技能可以通过训练改变。

马戏团中有以下常见技能

  • 火圈表演:单火圈、双火圈
  • 平衡表演:独木桥、走钢丝
  • 智慧表演:跳舞、算术题

狮子和老虎能进行火圈表演

猴子能进行平衡表演

狗能进行智慧表演

以上背景,先使用面向对象的方式实现一次。不考虑接口的模式

abstract class Animal {
    abstract type:string;
    constructor(
        public name:string,
        public age:number
    ){
    }
    sayHi(){
        console.log("hi")
    }
}

class Lion extends Animal {
    type = "lion"
    singleFire () {
        console.log("singleFire")
    }
     doubleFire () {
        console.log("doubleFire")
    }
}

class Tiger extends Animal {
    type = "tiger"
    
    singleFire () {
        console.log("singleFire")
    }
     doubleFire () {
        console.log("doubleFire")
    }
    
}

class Monkey extends Animal {
    type = "monkey"
    
    dumoqiao () {
        console.log("dumuqiao")
    }
    zougansi () {
        console.log("zougangsi")
    }
}

class Dog extends Animal {
    type = "dog",
    suanshuti () {
        console.log("suanshuti")
    }
}

上面代码乍一看没问题,但是老虎和狮子有相同的能力,但是实现能力的方式不一样,上面代码没有体现出有相同的能力,比如老虎的跳火圈方法写成singleFire,狮子的跳火圈方法写成Fire,上面的代码也不会报错。

场景案例继续走

动物们登场了

const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]

动物们打招呼

const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    val.sayHello()
})

打招呼没问题,因此打招呼的方法继承父类,

但是让多有会进行火圈的动物,完成跳火圈表演,这就不好实现了,因为没有一个标识,表明哪些动物会跳火圈,只能这样写了

const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    if(val instanceof Lion){
        val.singleFire()
        val.doubleFire()
    }else if (val instanceof Tiger){
        val.singleFire()
        val.doubleFire()
    }
})

上述代码有严重的隐患,因为判断条件是判断是不是会表演跳火圈,而实际的判断是判断老虎和狮子。当有一天马戏团来了新的狮子,此时狮子不会跳火圈,那这个判断条件问题出现了。

上述问题的根本原因在于

  • 对表演的能力没有强约束力

  • 容易将类型和能力耦合

系统中缺少对能力的定义 -- 接口

面向对象领域的接口的语意:表达了某个类是否拥有某种能力

使用interface对上面案例进行优化处理

interface IFireShow {
    singleFire():void;
    doubleFire():void;
}

interface IWisdomShow {
    suanshuti():void;
    tiaowu():void
}

interface IBalanceShow {
    dumuqiao():viod;
    zougangsi():viod
}

某个类具有某种能力其实就是实现接口,狮子具备跳火圈的能力

class Lion extends Animal implements IFireShow {
    type = "lion"
    singleFire () {
        console.log("singleFire")
    }                                  //不写,写错都会报错
     doubleFire () {
        console.log("doubleFire")
    }                                 //不写,写错都会报错
}

上述问题就解决了对表演的能力没有强约束力。

那么如何判断狮子是否具备有跳火圈的能力呢?使用类型保护处理

期望可以这样判断

const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    if(val instanceof IfireShow){
        val.singleFire()
        val.doubleFire()
    }
})

但是这种判断是运行时,所以受限ts在运行时是不参与编译的,所以没法通过上面的方式处理,希望ts以后会优化吧

写个类型保护函数吧

function hasFireShow(ani:object):ani is IFireShow {
    retun ((ani as unknown as IFireShow).singleFire && (ani as unknown as IFireShow).doubleFire)
}

再次优化代码

const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
    if(hasFireShow(val)){
        val.singleFire()
        val.doubleFire()
    }
})

还是有些戳,没办法,ts现在没有更好的办法,希望以后会优化吧

最后补充

  • 接口可以继承类,表示该类的所有成员都在接口中

        class Test1 {
            a = " "
            b = " "
        }
        class Test2 {
            a1 = " ",
            b1 = " ",
        }
        interface Test3 extends A,B {
        }
        // 相当于
        interface Test3 {
         a:string
         b:string
         a1:string
         b1:string
        }
        
    

七、装饰器

装饰器是面向对象的概念,在java中叫注解,在c#中叫特征,在ts中称为装饰器decorator,在angular中大量使用,目前js是支持装饰器的,但是处于建议阶段。

7.1 解决的问题

  • 分离关注点 以一个背景为例,创建一个用户类,
class User {
    id:string
}

现在有个需要需要验证用户的id不能大于10位数,第一种做法,创建一个验证函数

class User {
    id:string
    ...
}
const user = new User()

const validateUser(u:User){
    if(user.id?.length<=10){
    ...
    }
}

上面代码看起来没什么问题,但是换种场景就会发现有一些问题了,比如现在有两位同事进行开发,a同事开发了User这个类,b同事使用这个类,那么a同事一定是更加清楚这个id的使用验证规则的。所以上面代码还是不够清晰,好的有些同学可能要又会想到,可以在class 中书写

class User {
    id:string,
    validateUser(){
        if(this.id?.length<=10){
        ...
        }
    }
    ...
}
const user = new User()
user.validateUser()

上面代码看起来也没什么问题,但是细想,id这个属性还是和校验方法分开的当属性多了之后也是很烦的。

那么能不能在最开始定义这个id的时候就把校验规则加上呢?这种思维模式就是关注点的问题:在定义某些东西时,应该是最清楚该东西的情况的?

  • 重复代码问题

还是以上述为例,检验规则可能是5位也可能是15位等等,就会出现很多的重复if代码

上面两个问题产生的根源是,某些信息在定义时,能够附加的信息有限。 使用装饰器就可以为一些属性添加额外的信息

class User {
    @required
    @range(5,15)
    id:string,
    validateUser(){
        if(this.id?.length<=10){
        ...
        }
    }
    ...
}

通过上面的代码就知道了,装饰器的作用,为某些属性,类,参数,方法提供元数据信息。

7.1.1装饰器的本质

在js中装饰器就是一个函数。装饰器是会参与运行的。

装饰器可以修饰:

  • 类成员(属性+方法)
  • 参数

7.2、类装饰器

类修饰器的本质也是一个函数,该函数接收一个参数,表示类本身(构造函数本身) 注意在ts中要使用装饰器,需要在tsconfig.json中开启experimentalDecorators

装饰器函数运行时间,在类定义后直接运行 语法

@test1
class Test {
  constructor() {}
}

function test1(target: new () => object) {
  console.log(target);
}

上述代码编译后

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
let Test = class Test {
    constructor() { }
};
Test = __decorate([
    test1
], Test);
function test1(target) {
    console.log(target);
}

约束函数参数为类的ts

  • new ()=>object 约束class没有参数的类
  • new (...args:any[])=>object 约束class有参数的类

类装饰器的返回值

  • void:仅运行函数
  • 返回新的类,会将新的类替换掉装饰器的目标类

多装饰器调用顺序是从下到上

7.3成员装饰器

成员装饰器分为属性和方法

7.3.1 属性装饰器

属性装饰器本质也是一个函数,该函数需要两个参数,

参数一

  • 如果是静态属性参数为类本身
  • 如果是实例属性,则为类的原型

参数二

  • 属性名

7.3.2 方法装饰器

方法装饰器本质也是一个函数,该函数需要三个参数,

参数一

  • 如果是静态方法,则为类本身
  • 如果是实例方法,则为类原型

参数二

  • 固定为一个字符串,表示方法名

参数三

  • 属性描述符对象

7.4 reflect-metadata库

作用,在装饰编写代码的过程中,如果需要存入一些数据,一些开发者可能会存在对象的原型上,这样就会污染原型,因此引入reflect-metadata用来外部存储这些数据

具体使用参考官方文档,比较简单

7.5 class-validator 和 class-transformer库

class-validator:对类做一些验证,不成功则返回错误数据

class-transformer:把平面对象转换成class对象

7.6 装饰器补充

7.6.1 参数装饰器

使用场景,依赖注入设计模式中使用

要求函数有三个参数:

  • 参数一

    • 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
  • 参数二

    • 方法名称
  • 参数三

    • 参数列表索引
class Test {
    sum (a:number,@numberTest b:number) {
    
    }
}

function numberTest (target:any,method:string,index:number) {
}

依赖注入设计模式,后续一篇文章讲解

7.6.2 自动注入元数据

元数据:描述数据的数据

如果安装了reflect-metadata,并且导入了该库,并且在某个成员上添加了元数据。并且在tsconfig.json中启用了emitDecoratorMetadata 则ts在编译结果中,会将约束的类型,作为元数据加入到相应位置

这样一来ts的类型约束可以支持运行时了

目前使用场景很少、未来可能会发生大变化

7.6.3 AOP

属于面向对象的一种编程方式,将一些在业务中出现的功能块横向切分,分离关注点

八、索引器

const obj = {
    a:123
}
console.log(obj["a"])

这种读取对象值使用[]方式读取值的方式就是类型表达式

注意

ts中不会对类型表达式做严格的类型检查

class Test {
}
const test = new Test()

test["a"]      //不会报错,因此ts不会对类型表达式做严格的类型检查

如果期望做严格类型检查需要在tsconfig.jons中开启一个配置noImplicitAny:true

使用索引器代码修改

class Test {
    [prop:string]:any   //它会限制当前类的所有属性
}
test["a"]              //也不会报错了

在索引器中键可以是数字(数组)和字符串

索引器的作用

  • 可以在严格的检查模式下,实现类动态增加成员
  • 可以实现动态操作类成员

九、类型计算

根据已知信息,计算新的类型

9.1 三个关键字

  • typeof

ts中的typeof书写位置在类型约束的位置上,获取某个数据类型比如

const a = "123"
const b:typeof a ="2" //会报错,因为 typeof a 会得到一个字面量类型 '123',所以b只能是 "123"

注意当typeof主用于类(class)时,得到的是其构造函数

  • keyof

作用于class,interface,type用于获取其他类型的中成员组成的联合类型,比如

interface User {
    id:string,
    age:number
}
keyof User        // id:string | age:number

  • in

该关键字一般和keyof联用,限制某个索引的取值范围,比如

type Obj = {
    [p in "id"|"age"]:string
}
//等于
type Obj = {
    id:string,
    age:string
}

const obj:Obj ={
    id:"123",
    age:"123",
    hah:"123" //会报错
} 

有了上述的三个关键字,就可以编写一些ts类型工具了

9.2 常见工具类型

Partial<T> 把范型T的所有属性都变成可选的

type Partial<T>={
    [p in keyof T]?:T[p]
}

Required<T> 把范型T的所有属性都变成必选的

type Required<T>={
    [p in keyof T]-?:T[p]
}

ReadOnly<T> 把范型T的所有属性都变成只读的

type ReadOnly<T>={
   readonly [p in keyof T]:T[p]
}

String<T> 把范型T的所有属性都变成字符串

type String<T>={
    [p in keyof T]:string
}

Pick<T, K>

type Pick<T,K> = {
    [p in keyof T] : K[p]
}

从 T 中选取一组属性 K 构成新类型。

十、声明文件

10.1 什么是声明文件

.d.ts结尾的文件

10.2 声明文件的作用

为js代码提供类型声明

10.3 声明文件的位置

  • 放置到tsconfig.json包含的目录中
  • 放置到node_modules/@types文件目录中,一般都是npm安装的地方
  • 手动配置,在tsconfig.json文件中typeRoots中目录配置,这个配置之后,前面两种规则失效
  • 与js代码所在目录相同,并且文件名也相同的文件,这种格式其实就是ts工程发布之后的格式

10.4 编写声明文件

  • 手动编写

    手动编写的场景

    • 已有的库,它是使用的js书写而成,并且更改该库代码为ts成本较高
    • 对一些第三方的库,它们使用js写的,并且这些三方库没有提供声明文件,可以手动编写声明文件
  • 自动生成

    • 工程如果是使用ts生成的,发布编译后的js文件,如果发布的文件需要被别人使用,别人也需要类型检查,那么此时可以使用声明文件来描述发布结果结果中的类型
    • 此时ts工程就可以使用自动生成,在tsconfig.json中配置declaration即可

全局声明

declare var console {
    log:(message?:any)=>void
}

namespace:表示命名空间,可以将其认为是一个对象,命名空间中的内容必须通过命名空间.成员名访问

declare namespace console {
    log:(message?:any)=>void
}

十一、tslint

tslint和eslint差不多,tslint用来检查ts的代码规范的

一般安装到工程内

npm install - D tslint typescript

初始化类型检查配置文件

npx tslint --init

可以根据官网配置相应的检查规则

🤔「`interface` 和 `type` 到底用哪个?」——几乎每个 TS 新手被这个选择灵魂拷问。

开场白:为什么又写这个话题?

interfacetype 到底用哪个?」——几乎每个 TS 新手在 StackOverflow 上都会刷到这条灵魂拷问。
官方文档只有一句 “they are largely equivalent”,却藏了 10 多个细节坑:
有人因为用了 type 导致无法给第三方库“打补丁”;有人因为滥用 interface 把编译器拖成“风扇狂魔”;还有人把联合类型写进 interface 直接爆红。

1. 一句话先记住

interface 是“可扩展的结构性契约”;type 是“类型层面的万能表达式”。
——先写 interface,做不到再请 type 出山,基本不会犯错。

2. 速查表(收藏级)

场景 推荐 例子
纯对象形状 interface interface User { name: string }
联合/元组 type type Status = 'ok' | 'error'
原始类型别名 type type ID = string
映射/条件类型 type type Partial<T> = { [K in keyof T]?: T[K] }
需要声明合并 interface window 补属性
类 implements interface class Cat implements Animal
性能敏感巨型字典 interface 10w+ 键的 AST 节点
工具类型二次加工 type type CreateSlice<S> = Pick<Store<S>, 'getState'>

3. 10 维度硬核对比

3.1 语法能力

interface 只能描述对象、函数、类构造签名
type 可以描述任意类型组合:联合、交叉、元组、映射、条件、模板字面量……

// 联合
type Pet = 'cat' | 'dog';
// 元组
type Coord = [number, number];
// 条件
type IsArray<T> = T extends any[] ? true : false;

→ 需要“组合/变形”时,直接上 type

3.2 声明合并(Declaration Merging)

interface User { name: string; }
interface User { age: number; }   // 自动合并
type User = { name: string; };
type User = { age: number; };     // ❌ 重复标识符

给第三方库补类型、扩展 windowglobal 必须 interface

3.3 同名属性冲突

interface A { x: number; }
interface B extends A { x: string; }  // ❌ 直接报错

type A = { x: number };
type B = A & { x: string };           // 不报错,但 x 成 never

interface 提前暴露错误;type 把问题推迟到使用点。

3.4 循环引用

interface Tree { left: Tree; right: Tree; }   // ✅ 直接递归

type Tree = { left: Tree; right: Tree; };     // ❌ 循环引用报错

想写链表、树、图,优先 interface

3.5 编译性能

  • interface 采用名义+结构混合缓存,超大项目检查更快。
  • type 的深层交叉/联合可能触发指数展开,10w+ 节点场景差距明显。
    微软 TS Wiki 原话:

“Use interfaces until you need to use features from type aliases.”

3.6 映射类型 & 工具类型

PartialRecordExcludeReturnType… 全部用 type 实现,无法用 interface 表达

3.7 可读性

type F = ((x: 'a') => 1) & ((x: 'b') => 2) extends infer R ? R : never;

这种“一行炸出 5 个关键字”的代码,用 interface 根本写不出来,也更容易把同事劝退。
公共 API 优先 interface,内部黑魔法再包 type。

3.8 与 class 共舞

interface Clock { tick(): void; }
class A implements Clock { tick() {} }   // 语义贴合

implements 两者都支持,但 interface 更契合“契约”思想。

3.9 模块导出

无差异,都能 export

3.10 官方未来路线

RFC 曾讨论“让 interface 支持联合”,被否决;官方仍倾向 interface 做“结构契约”

4. 实战决策树(复制即可)

需要联合/元组/映射/条件/原始别名?
├─ 是 ──> 用 type
└─ 否 ──> 纯对象?
        ├─ 是 ──> 需要声明合并或 implements?
        │       ├─ 是 ──> interface
        │       └─ 否 ──> 可 interface 也可 type,**默认 interface**
        └─ 否 ──> type

5. 典型案例:写组件 Props

// 1. 先写契约
interface BaseProps {
  title: string;
  onClose: () => void;
}

// 2. 需要部分可选,再包一层 type
type DrawerProps = Partial<BaseProps> & {
  width?: number;
};

// 3. 导出给外部
export { DrawerProps };

既享受合并能力,又拿到工具类型的便利。

6. 小结:一句顺口溜

“对象先 interface,变形上 type;合并靠 interface,联合找 type。”

把这张速查表贴到仓库 Wiki,下次 Code Review 就不用再拉群辩论了。

如果这篇文章帮到了你,点个 ⭐ 再走呗~ 评论区聊聊:你们团队是“interface 党”还是“type 党**”?**

TypeScript的新类型(五):tuple元组

定义

元组(tuple)是⼀种特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型是已知的且可以不同。元组⽤于精确描述⼀组值的类型,?表示可选元素。

  • 注意: 在ts中tuple 不是关键词,只是⼀种 特殊的数组 形式
  • 可以存储固定数量元素,且元素类型已定义并且可以不同
  • 给元组赋值时元素的个数元素类型都要符合定义时候的声明(除了 ? 可选元素和 ...元素类型[] 任意数量元素情况)
// 第⼀个元素必须是 string 类型,第⼆个元素必须是 number 类型。
let arr1: [string,number]
arr1 = ['hello',123]
// 不可以赋值,arr1声明时是两个元素,赋值的是三个
arr1 = ['hello',123,false]

?可选元素的定义

  • ?加在元素后面,表示该属性为可选元素
// 第⼀个元素必须是 number 类型,第⼆个元素是可选的,如果存在,必须是 boolean 类型。
let arr2: [number,boolean?]
arr2 = [100,false]
arr2 = [200]

...元素类型[],任意数量的元素的定义

  • ...string[]允许元组有任意数量的元素,在...后元素的数据类型
// 第⼀个元素必须是 number 类型,后⾯的元素可以是任意数量的 string 类型
let arr3: [number,...string[]]
arr3 = [100,'hello','world']
arr3 = [100]
❌