Skip to content

教学教程 —— 从零学会写汉语 TypeScript ​

⬇ ChineseTypeScript 教学教程 —— 下载 Word 版(.docx) 适合打印、发邮件、或发给不习惯看网页的人。内容与本页相同。

这本教程教你三件事:汉语关键字怎么用(每个都给出对应的英文)、 原本那套符号怎么用({} () ; => ?. ?? … 一个没换)、 基础标准库怎么用(这一条有个重要前提,见下)。

⚠ 与 Chinese++ 的一个重大差别:Chinese++ 连标准库都做了汉化层 (#包含 <向量>、标准::向量<整数>)。ChineseTypeScript 只汉化了语言关键字, console / Array / Map / JSON 这些运行库的名字没有中文别名, 一律照写英文(英文永远可用)。

本文件是生成物,不要手改。 由 tools\make_tutorial_chts.ps1 把 教程\例子\*.ts 的真源码与「期望输出」切进 教程\教程模板.md 生成。 tests-chts\judge_tutorial.ps1 会真编译真运行每个例子并核对输出, 同时核对本文件与例子是否逐字一致 —— 所以教程里的代码必然能跑。


怎么用这本教程 ​

  1. 先读「第 0 课」把编译命令跑通。
  2. 每课都有一份能直接编译运行的例子,放在 教程\例子\。
  3. 例子开头的注释里写了它自己的期望输出 —— 你跑出来的应当一模一样。
  4. 汉语写法和英文写法可以混着写,同一行里混都行。

第 0 课 准备:编、跑 ​

编译器:E:\cts-build\chtsc.exe(用 tools\build_chtsc.ps1 构建;名字见「命令名」一节)。

powershell
chtsc 你好.ts --outDir 输出 --target es2020 --module commonjs
node 输出\你好.js

一个最小程序长这样(第 1 课细讲):

ts
函数 打招呼(): 空型 {
  console.log("你好,世界");
}
打招呼();

第 1 课 程序骨架 ​

本课关键字 ​

汉语英文干什么
函数function定义函数
返回return从函数返回
空型void「什么也不返回」
数字number数字类型(TypeScript 的数字都是浮点)

本课符号 ​

符号名称用法
{ }花括号包住一段代码(函数体、分支体)
( )圆括号参数表 / 调用函数
:冒号类型标注:甲: 数字 读作「甲,类型是数字」
;分号语句末尾。TypeScript 里可以省,但建议写(本教程一律写)
//行注释到这里行尾都不算代码
" "双引号字符串
+加号数字相加;字符串拼接也用它

例子的真源码 ​

ts
// 第 1 课:程序骨架
//
// 关键字(英文对照):函数(function) 返回(return) 空型(void) 数字(number)
// 符号:{ } 包代码块、( ) 参数表、; 语句结束、: 类型标注、// 注释、"" 字符串
//
// 期望输出:
// 你好,世界
// 汉语编程
// 两数之和=7

函数 相加(甲: 数字, 乙: 数字): 数字 {
  返回 甲 + 乙;                 // 返回 = return
}

函数 打招呼(): 空型 {           // 空型 = void:不返回东西
  console.log("汉语编程");
}

console.log("你好,世界");
打招呼();
console.log("两数之和=" + 相加(3, 4));

跑出来的结果(与例子里的「期望输出」一致):

text
你好,世界
汉语编程
两数之和=7

讲解 ​

  • console.log(...) 是打印。console 是运行库的名字,没有中文别名。
  • 函数签名读法:函数 相加(甲: 数字, 乙: 数字): 数字 —— 名字、参数(每个参数 名字: 类型)、最后是返回类型。
  • TypeScript 不像 C++ 那样要求有个 main;文件从上往下执行。

练习 ​

  1. 写一个 函数 相乘(甲: 数字, 乙: 数字): 数字,打印 相乘(3, 4)。
  2. 把 函数 写成英文 function,确认结果一样(中英可混写)。

第 2 课 变量与类型 ​

本课关键字 ​

汉语英文干什么
常量const之后不许重新赋值(首选)
局部变量let可以重新赋值,作用域限制在块里
变量var老写法,作用域是整个函数(新代码别用)
接口interface描述一个对象长什么样
类型别名type给类型起名字
数字 字符串 布尔number string boolean基本类型
真值 / 假值true / false布尔字面量
空值null「空」 —— 值和类型两种位置都能用
未定义undefined「未定义」 —— 当类型可以,当值要写英文(附录 C)
任意 未知 永不any unknown never特殊类型

本课符号 ​

符号用法
:类型标注:常量 年龄: 数字 = 18;
=赋值
[]数组类型 数字[];取下标 表[0](从 0 开始)
|联合类型:字符串 | 未定义 读作「字符串或未定义」
?可选属性:名字?: 字符串
[ ]元组类型:[字符串, 数字] —— 按位置标类型
{ }对象字面量

例子的真源码 ​

ts
// 第 2 课:变量与类型
//
// 关键字(英文对照):
//   常量(const) 局部变量(let) 变量(var) 类型别名(type) 接口(interface)
//   数字(number) 字符串(string) 布尔(boolean) 真值(true) 假值(false)
//   空值(null) 未定义(undefined) 任意(any) 未知(unknown) 永不(never)
// 符号:: 类型标注、= 赋值、[] 数组、| 联合类型、{ } 对象字面量
//
// 期望输出:
// 年龄=18
// 身高=1.75
// 名字=小明
// 已成年=true
// 表长度=3
// 元组=甲,1
// 对象=小红,20
// 联合类型=已启用
// 空值类型=object
// 未定义类型=undefined
//
// 注:`空值`(null) 与 `未定义`(undefined) 当【值】和当【类型】都可以。
//     要记住的是另一件事:属性名与成员名里的汉语词是运行时的真实属性名,
//     不会被翻译(`{ 未定义: 1 }` 的属性名就叫「未定义」,见第 9 课与附录 C)。

常量 年龄: 数字 = 18;                    // 常量 = const
局部变量 身高 = 1.75;                    // 局部变量 = let;类型由右边的值推出来
变量 名字: 字符串 = "小明";               // 变量 = var(老写法,新代码请用 常量/局部变量)
常量 已成年: 布尔 = 真值;                 // 真值 = true
常量 表: 数字[] = [1, 2, 3];              // 数字[] = 「数字的数组」
常量 元组: [字符串, 数字] = ["甲", 1];     // 元组:类型按位置写

接口 人 { 名字: 字符串; 年龄: 数字 }        // 接口 = interface
常量 对象: 人 = { 名字: "小红", 年龄: 20 };

类型别名 状态 = "启用" | "停用";           // 类型别名 = type;| 是联合类型
常量 当前: 状态 = "启用";

常量 空的 = 空值;                         // 空值 = null(当值用)
常量 空着 = 未定义;                      // 未定义 = undefined(值位置同样可用)

console.log("年龄=" + 年龄);
console.log("身高=" + 身高);
console.log("名字=" + 名字);
console.log("已成年=" + 已成年);
console.log("表长度=" + 表.length);        // .length 是 JS 内建的成员,英文
console.log("元组=" + 元组[0] + "," + 元组[1]);
console.log("对象=" + 对象.名字 + "," + 对象.年龄);
console.log("联合类型=" + (当前 === "启用" ? "已启用" : "已停用"));
console.log("空值类型=" + typeof 空的);
console.log("未定义类型=" + typeof 空着);

跑出来的结果:

text
年龄=18
身高=1.75
名字=小明
已成年=true
表长度=3
元组=甲,1
对象=小红,20
联合类型=已启用
空值类型=object
未定义类型=undefined

讲解 ​

  • 优先用 常量:能不变就不变。需要重新赋值才用 局部变量。
  • 局部变量 身高 = 1.75; 没写类型 —— TypeScript 自己推出来了(推断)。
  • 接口 只描述形状,不生成代码;类型别名 用来给联合类型等起名字。
  • .length 是 JS 内建的成员,数的是 UTF-16 码元(一个汉字算 1)。

练习 ​

  1. 定义一个 接口 书 { 书名: 字符串; 页数: 数字 } 并创建一个常量。
  2. 用 类型别名 定义一个「数字或字符串」的联合类型并各用一个值。

第 3 课 运算符与符号 ​

本课最重要的一句话:符号一个都没换。 注意与 C++ 的差别 —— C++ 有「运算符替代拼写」(并且/或者/并非), TypeScript 这边没有,&& || ! 照写。

本课符号 ​

类别符号说明
算术+ - * / %+ 也用于字符串拼接
比较=== !== == != < > <= >=推荐 === / !==(严格相等,不做类型转换)
逻辑&& || !与或非
空值合并??左边是 空值/undefined 时才取右边
可选链?.左边是空就整个表达式给 undefined,不报错
三元? :条件 ? 真时 : 假时
赋值= += -= *= /=甲 += 1 等价 甲 = 甲 + 1
自增自减++ --
展开/剩余...[...表甲, 3] 把数组摊开;函数参数里把剩下的收起来
模板串` 与 ${}`你好,${名字}`
类型相关: | & <> ? !标注、联合、交叉、泛型、可选、非空断言

例子的真源码 ​

ts
// 第 3 课:运算符与符号
//
// 本课最重要的一句话:**TypeScript 这边符号一个都没换。**
// 而且它跟 C++ 那边不同 —— C++ 有「运算符替代拼写」(`并且`/`或者`/`并非`),
// TypeScript 这边【没有】,`&&` `||` `!` 照写。
//
// 关键字(英文对照):常量(const) 数字(number) 字符串(string) 布尔(boolean)
//   真值(true) 假值(false) 未定义(undefined) 空值(null) 接口(interface) 类型别名(type)
// 符号:算术 + - * / %、比较 === !== == != < > <= >=、逻辑 && || !、
//       空值合并 ??、可选链 ?.、三元 ? :、展开 ...、模板串 ` 与 ${}
//
// 期望输出:
// 算术=10 4 21 2 1
// 比较=true false true
// 严格相等=true 松散相等=true
// 逻辑=false true false
// 空值合并=默认值
// 可选链=undefined
// 三元=较大
// 展开=[1,2,3,4]
// 模板串=你好,小明!你有2条消息

常量 甲: 数字 = 7;                  // 显式写 : 数字,免得被推成字面量类型 7
常量 乙: 数字 = 3;

// —— 算术:+ - * / % 全部照写 ——
console.log("算术=" + (甲 + 乙) + " " + (甲 - 乙) + " " + (甲 * 乙) + " " + Math.floor(甲 / 乙) + " " + (甲 % 乙));

// —— 比较:=== 严格相等(推荐),== 松散相等 ——
console.log("比较=" + (甲 > 乙) + " " + (甲 < 乙) + " " + (甲 !== 乙));
console.log("严格相等=" + (甲 === 7) + " 松散相等=" + (甲 == 7));

// —— 逻辑:&& || ! 照写(没有汉语替代拼写)——
console.log("逻辑=" + (真值 && 假值) + " " + (真值 || 假值) + " " + (!真值));

// —— 空值合并 ??:左边是 空值 或 undefined 时才用右边 ——
常量 可能空: 字符串 | 未定义 = 未定义;      // 类型位与值位都可以写 未定义
console.log("空值合并=" + (可能空 ?? "默认值"));

// —— 可选链 ?.:左边是 空值/undefined 就整个表达式给 undefined,不报错 ——
接口 配置 { 名字?: 字符串 }                  // ? 表示「这个属性可以没有」
常量 配置甲: 配置 = {};
console.log("可选链=" + 配置甲.名字?.length);

// —— 三元 ? : ——
console.log("三元=" + (甲 > 乙 ? "较大" : "较小"));

// —— 展开 ...(把数组/对象摊开)——
常量 表甲 = [1, 2];
常量 表乙 = [...表甲, 3, 4];
console.log("展开=[" + 表乙.join(",") + "]");

// —— 模板串:反引号包起来,${} 里可以放任何表达式 ——
常量 名字 = "小明";
常量 条数 = 2;
console.log(`模板串=你好,${名字}!你有${条数}条消息`);

跑出来的结果:

text
算术=10 4 21 2 1
比较=true false true
严格相等=true 松散相等=true
逻辑=false true false
空值合并=默认值
可选链=undefined
三元=较大
展开=[1,2,3,4]
模板串=你好,小明!你有2条消息

讲解 ​

  • === 和 == 的区别:=== 要求类型也相同,== 会做类型转换。 写 === 能避开一整类古怪的 bug。
  • ?? 与 || 的区别:|| 会把 0、"" 也当成「假」, ?? 只在 空值/undefined 时才用右边 —— 处理「可能没配」的值时用 ??。
  • 模板串用反引号(键盘左上角那个),变量写在 ${} 里。

练习 ​

  1. 用 ?? 给一个「可能没有的配置项」设默认值。
  2. 用模板串打印 你好,${名字}!。

第 4 课 分支与循环 ​

本课关键字 ​

汉语英文干什么
如果 / 否则if / else分支
选择switch多分支
情形case选择里的一个分支
默认default选择的兜底分支
对于for计次循环
遍历of对于 (常量 项 遍历 表) —— 遍历元素
属于in「某个键在不在对象里」
每当while先判断再执行
执行do先执行一次再判断(搭配 每当 收尾)
跳出 / 跳过break / continue结束整个循环 / 跳过本轮

本课符号 ​

写法说明
如果 (条件) { … }条件用 ( ) 包起来
如果 … 否则 { … }否则 前不写分号
选择 (值) { 情形 1: … 跳出; 默认: … }情形 后是冒号,每个 情形 末尾要 跳出
对于 (初值; 条件; 步进) { … }三段用分号隔开
执行 { … } 每当 (条件);do-while:末尾那个 每当 后面必须有 ;
对于 (常量 项 遍历 容器) { … }范围遍历

例子的真源码 ​

ts
// 第 4 课:分支与循环
//
// 关键字(英文对照):
//   如果(if) 否则(else) 选择(switch) 情形(case) 默认(default)
//   对于(for) 遍历(of) 属于(in) 每当(while) 执行(do)
//   跳出(break) 跳过(continue) 常量(const) 局部变量(let) 数字(number)
// 符号:() 包条件、{ } 包代码块、; 结束语句、: 跟在 情形/默认 后、++ 自增
//
// 期望输出:
// 及格
// 等级=C
// 累加=10
// 每当两遍后=2
// 执行后=2
// 跳过偶数后=1 3 5
// 遇到5就跳出=1 2 3 4
// 遍历=3 1 2
// 属于=true

常量 分数 = 75;

// —— 如果 / 否则 ——
如果 (分数 >= 60) {
  console.log("及格");
} 否则 {
  console.log("不及格");
}

// —— 选择 / 情形 / 默认:每个情形末尾要写 跳出,否则会往下穿 ——
常量 档 = Math.floor(分数 / 10);
选择 (档) {
  情形 9: console.log("等级=A"); 跳出;
  情形 8: console.log("等级=B"); 跳出;
  情形 7: console.log("等级=C"); 跳出;
  默认:   console.log("等级=D");
}

// —— 对于(三段用分号隔开)——
局部变量 累加 = 0;
对于 (局部变量 甲 = 1; 甲 <= 4; 甲++) {
  累加 += 甲;
}
console.log("累加=" + 累加);

// —— 每当(先判断再执行)——
局部变量 遍 = 0;
每当 (遍 < 2) {
  遍++;
}
console.log("每当两遍后=" + 遍);

// —— 执行…每当(先执行一次再判断)——
局部变量 次 = 0;
执行 {
  次++;
} 每当 (次 < 2);
console.log("执行后=" + 次);

// —— 跳过:跳过本轮剩下的 ——
局部变量 跳过输出 = "跳过偶数后=";
对于 (局部变量 甲 = 1; 甲 <= 6; 甲++) {
  如果 (甲 % 2 === 0) 跳过;
  跳过输出 += 甲 + " ";
}
console.log(跳过输出.trimEnd());

// —— 跳出:直接结束整个循环 ——
局部变量 跳出输出 = "遇到5就跳出=";
对于 (局部变量 甲 = 1; 甲 <= 9; 甲++) {
  如果 (甲 === 5) 跳出;
  跳出输出 += 甲 + " ";
}
console.log(跳出输出.trimEnd());

// —— 遍历:对于 (常量 项 遍历 容器) ——
常量 表 = [3, 1, 2];
局部变量 遍历输出 = "遍历=";
对于 (常量 项 遍历 表) {
  遍历输出 += 项 + " ";
}
console.log(遍历输出.trimEnd());

// —— 属于:某个键在不在对象里 ——
常量 人 = { 名字: "小明", 年龄: 18 };
console.log("属于=" + ("名字" 属于 人));

跑出来的结果:

text
及格
等级=C
累加=10
每当两遍后=2
执行后=2
跳过偶数后=1 3 5
遇到5就跳出=1 2 3 4
遍历=3 1 2
属于=true

讲解 ​

  • 选择 里的 情形 是值(数字或字符串),比较用 === 的语义。
  • 忘了 跳出 会一路穿到下一个 情形 去(这叫 fallthrough)。
  • 遍历 拿的是元素;属于 查的是键。这两个别搞混。

练习 ​

  1. 用 选择 把 1~7 打印成「星期一」到「星期日」。
  2. 用 每当 与 对于 各算一次 1 到 100 的和,对照结果。

第 5 课 函数 ​

本课关键字 ​

汉语英文干什么
函数function定义函数
返回return返回值
空型void不返回值
常量 局部变量const let函数体里的变量
任意 未知any unknown特殊类型

本课符号 ​

符号用法
( ) ,参数表、分隔参数
: 类型参数类型、返回类型
=>箭头函数:常量 加 = (甲: 数字) => 甲 + 1;
?可选参数:函数 问候(名字?: 字符串)
= 值默认参数:函数 加上(甲: 数字, 乙: 数字 = 3)
...剩余参数:函数 汇总(...数: 数字[])
< >泛型参数:函数 取大<甲类型>(…)

例子的真源码 ​

ts
// 第 5 课:函数
//
// 关键字(英文对照):
//   函数(function) 返回(return) 空型(void) 数字(number) 字符串(string)
//   局部变量(let) 常量(const) 类型别名(type) 任意(any) 未知(unknown)
// 符号:
//   ( )        参数表 / 调用
//   : 类型      参数与返回值的类型标注
//   =>         箭头函数
//   ?          可选参数(调用时可以不传)
//   = 值       默认参数
//   ...        剩余参数(把剩下的都收进一个数组);用在调用处则是展开
//   < >        泛型参数
//
// 期望输出:
// 相加=7
// 箭头相加=7
// 默认参数=10
// 可选参数=你好
// 汇总=10
// 泛型取大=9
// 高阶函数=6

// —— 普通函数:参数和返回值都标类型 ——
函数 相加(甲: 数字, 乙: 数字): 数字 {
  返回 甲 + 乙;
}
console.log("相加=" + 相加(3, 4));

// —— 箭头函数:=> 读作「变成」——
常量 箭头相加 = (甲: 数字, 乙: 数字): 数字 => 甲 + 乙;
console.log("箭头相加=" + 箭头相加(3, 4));

// —— 默认参数:不传就用 = 后面的值 ——
函数 加上(甲: 数字, 乙: 数字 = 3): 数字 {
  返回 甲 + 乙;
}
console.log("默认参数=" + 加上(7));

// —— 可选参数:名字后面写 ? ——
函数 问候(名字?: 字符串): 字符串 {
  返回 "你好" + (名字 ? "," + 名字 : "");
}
console.log("可选参数=" + 问候());

// —— 剩余参数:... 把剩下的实参收成一个数组 ——
函数 汇总(...数: 数字[]): 数字 {
  局部变量 和 = 0;
  对于 (常量 项 遍历 数) {
    和 += 项;
  }
  返回 和;
}
console.log("汇总=" + 汇总(1, 2, 3, 4));

// —— 泛型:一份代码服务多种类型 ——
函数 取大<甲类型>(甲: 甲类型, 乙: 甲类型): 甲类型 {
  返回 甲 > 乙 ? 甲 : 乙;
}
console.log("泛型取大=" + 取大(9, 3));

// —— 函数可以当参数传(高阶函数)——
函数 两次(做: (值: 数字) => 数字, 初值: 数字): 数字 {
  返回 做(做(初值));
}
console.log("高阶函数=" + 两次((值: 数字) => 值 + 1, 4));

跑出来的结果:

text
相加=7
箭头相加=7
默认参数=10
可选参数=你好
汇总=10
泛型取大=9
高阶函数=6

讲解 ​

  • 函数是一等公民:可以当参数传、可以当返回值(例子最后一段就是)。
  • => 读作「变成」:(值) => 值 + 1 是「给它一个值,返回它加一」。
  • 泛型 <甲类型> 让一份代码服务多种类型;调用时通常不用写类型实参。

练习 ​

  1. 写一个带默认参数的函数并调用两次(一次传、一次不传)。
  2. 写一个把数组里每个数翻倍的高阶函数(参数是一个函数)。

第 6 课 类与接口 ​

本课关键字 ​

汉语英文干什么
类型class定义类(注意:不是「类」,汉语关键字不少于两个字)
接口interface描述形状
继承extends继承父类
实现implements声明实现某个接口
构造器constructor构造方法
自身this当前对象
父类super父类
重写override声明「我改写了父类的成员」
公开 私有 受保护public private protected访问控制
只读readonly构造之后不许改
静态static属于类本身
取值 / 设值get / set取值器 / 设值器
抽象abstract抽象类/抽象方法
新建new创建实例

本课符号 ​

写法说明
类型 名字 { … }类体
公开 名字: 类型;属性声明(访问修饰符 + 名字 + 冒号 + 类型)
继承 父类 实现 接口都用空格,不像 C++ 那样用冒号
自身.成员访问自己
父类(…)调用父类构造器

例子的真源码 ​

ts
// 第 6 课:类与接口
//
// 关键字(英文对照):
//   类型(class) 接口(interface) 继承(extends) 实现(implements)
//   构造器(constructor) 自身(this) 重写(override)
//   公开(public) 私有(private) 受保护(protected) 只读(readonly) 静态(static)
//   取值(get) 设值(set) 抽象(abstract) 新建(new) 空型(void) 返回(return)
// 符号:
//   { } 类体      ; 类成员之间用分号(或换行)
//   : 类型        属性/参数/返回值的类型标注
//   => 箭头       方法也可以用箭头写
//   继承 / 实现 都用【空格】,不像 C++ 那样用冒号
//
// 期望输出:
// 名字=小猫 叫声=喵
// 面积=9
// 只读号数=7
// 取值器=10
// 静态计数=1
// 抽象实现面积=9
// 继承来的=我是小猫

// —— 接口:描述「长什么样」——
接口 会叫 {
  叫(): 字符串;
}

// —— 类型(class)——
类型 动物 {
  公开 名字: 字符串;                 // 公开 = public(不写也是公开)
  私有 秘密 = "藏起来";              // 私有 = private:外面访问不到
  受保护 家族 = "动物界";            // 受保护 = protected:子类能访问
  只读 号数: 数字 = 7;               // 只读 = readonly:构造后不许改
  静态 计数 = 0;                     // 静态 = static:属于类型本身

  构造器(名字: 字符串) {             // 构造器 = constructor
    自身.名字 = 名字;                // 自身 = this
   }

  自我介绍(): 字符串 {
    返回 "我是" + 自身.名字;
  }
}

// —— 继承 ——
类型 猫 继承 动物 实现 会叫 {        // 继承 = extends;实现 = implements
  构造器() {
    父类("小猫");                    // 父类 = super:调用父类构造器
  }
  叫(): 字符串 {
    返回 "喵";
  }
}

// —— 抽象类:自己不能直接 new ——
抽象 类型 形状 {
  抽象 面积(): 数字;                 // 抽象方法:子类必须实现
}

类型 正方形 继承 形状 {
  边: 数字 = 3;
  面积(): 数字 {
    返回 自身.边 * 自身.边;
  }
}

// —— 取值器 / 设值器 ——
类型 带取值器 {
  私有 值 = 5;
  取值 十倍(): 数字 {                // 取值 = get
    返回 自身.值 * 2;
  }
  设值 十倍(新值: 数字) {            // 设值 = set
    自身.值 = 新值 / 2;
  }
}

常量 猫甲 = 新建 猫();                        // 新建 = new
console.log("名字=" + 猫甲.名字 + " 叫声=" + 猫甲.叫());

常量 方 = 新建 正方形();
console.log("面积=" + 方.面积());

console.log("只读号数=" + 猫甲.号数);

常量 取值器 = 新建 带取值器();
console.log("取值器=" + 取值器.十倍);

动物.计数 = 1;
console.log("静态计数=" + 动物.计数);

常量 形: 形状 = 方;
console.log("抽象实现面积=" + 形.面积());

console.log("继承来的=" + 猫甲.自我介绍());

跑出来的结果:

text
名字=小猫 叫声=喵
面积=9
只读号数=7
取值器=10
静态计数=1
抽象实现面积=9
继承来的=我是小猫

讲解 ​

  • 类型 与 C++ 的 class 一样,但 TypeScript 里 extends 是关键字 继承, 写法是 类型 猫 继承 动物(空格分隔),不是 :。
  • 私有 成员只能在类内部访问;受保护 子类也能访问。
  • 抽象 类型 不能直接 新建,只能被继承;抽象方法子类必须实现。
  • 取值 十倍() 用的时候不加括号:取值器.十倍(例子就是这么打印的)。

练习 ​

  1. 写一个 类型 点 { 横: 数字; 纵: 数字 } 加一个 长度(): 数字(横纵平方和的平方根)。
  2. 让一个子类 继承 它并 重写 长度。

第 7 课 泛型与名域 ​

本课关键字 ​

汉语英文干什么
名域namespace给一组东西起个名字,避免撞名
导出export导出给别人用
导入import导入别人的东西
来自from导入 { 甲 } 来自 "./模块"
作为as起别名:导入 { 甲 作为 乙 } / 类型断言 值 作为 类型
类型别名type也可以带泛型
接口interface也可以带泛型

本课符号 ​

符号用法
< >泛型参数与实参:函数 取大<元素类型>(…)
.访问名域成员:工具.值
{ }名域体、导入列表
" "模块路径(必须是字符串,写英文/原样)

例子的真源码 ​

ts
// 第 7 课:泛型与名域
//
// 关键字(英文对照):
//   函数(function) 返回(return) 数字(number) 字符串(string)
//   类型别名(type) 接口(interface) 名域(namespace) 导出(export)
//   导入(import) 来自(from) 作为(as) 常量(const) 局部变量(let) 任意(any)
// 符号:
//   < >      泛型参数 / 类型实参
//   ::       访问名域里的成员(TypeScript 用 . 也行,见例子)
//   { }      名域体
//
// 期望输出:
// 泛型数组=1,7,3
// 泛型取大=9
// 名域里的值=42
// 名域里的函数=42
// 受限泛型=名字长度=2
//
// 说明:本课是【单文件】例子,所以没有真的 import。
//       导入/导出的可跑例子在 `tests-chts/proj/`(主.ts 与 数学.ts),
//       它由 `tests-chts/judge_proj.ps1` 真编译真运行。

// —— 泛型:一份代码服务多种类型 ——
函数 首项<元素类型>(表: 元素类型[]): 元素类型 | 未定义 {
  返回 表.length > 0 ? 表[0] : undefined;
}

函数 取大<元素类型>(甲: 元素类型, 乙: 元素类型): 元素类型 {
  返回 甲 > 乙 ? 甲 : 乙;
}

// —— 泛型带约束:要求这个类型必须有 length ——
接口 有长度 { length: 数字 }
函数 量长度<元素类型 继承 有长度>(东西: 元素类型): 数字 {
  返回 东西.length;
}

console.log("泛型数组=" + [首项([1, 2, 3]), 首项([7, 8]), 3].join(","));
console.log("泛型取大=" + 取大(9, 3));

// —— 名域:给一组东西起个名字,避免撞名 ——
名域 工具 {
  导出 常量 值 = 42;
  导出 函数 取(): 数字 {
    返回 工具.值;
  }
}

console.log("名域里的值=" + 工具.值);
console.log("名域里的函数=" + 工具.取());

// —— 类型别名也可以泛型 ——
类型别名 表<元素类型> = 元素类型[];
常量 名字表: 表<字符串> = ["甲", "乙"];
console.log("受限泛型=名字长度=" + 量长度(名字表));

跑出来的结果:

text
泛型数组=1,7,3
泛型取大=9
名域里的值=42
名域里的函数=42
受限泛型=名字长度=2

讲解 ​

  • 泛型约束写法:<元素类型 继承 有长度> —— 要求这个类型至少具备 有长度 的形状。
  • 导入/导出的可跑例子不在本课(本课是单文件):见 tests-chts\proj\ 里的 主.ts 与 数学.ts,它由判定脚本真编译真运行。
  • 模块路径必须原样写("./数学"),它是运行时要解析的字符串,翻译了就找不到文件。

练习 ​

  1. 写一个泛型 函数 末项<元素类型>(表: 元素类型[])。
  2. 在自己的 名域 里放两个函数,用 . 调用它们。

第 8 课 基础标准库(诚实版) ​

⚠ ChineseTypeScript 没有「汉语标准库」。 与 Chinese++ 不同,这边只汉化了 语言关键字;console / Array / Map / JSON / Math 都是运行库的名字, 没有中文别名,一律写英文。

本课的关键字只出现在语法位置(常量 / 局部变量 / 遍历 / 函数 …), 库的成员名(.map / .filter / .length / .slice)全部照写英文。

例子的真源码 ​

ts
// 第 8 课:基础标准库怎么用(诚实版)
//
// ⚠ 先说清楚:**ChineseTypeScript 没有「汉语标准库」。**
//   这一点与 Chinese++ 不同 —— 那边连标准库都做了汉化层(`#包含 <向量>`、
//   `标准::向量<整数>` 都能用)。TypeScript 这边只汉化了**语言关键字**,
//   `console` / `Array` / `Map` / `JSON` 这些是**运行库的名字**,没有中文别名。
//   所以本课的关键字只在语法位置出现,库的成员名一律写英文 —— 英文永远可用。
//
// 本课关键字(英文对照):常量(const) 局部变量(let) 函数(function) 返回(return)
//   数字(number) 字符串(string) 遍历(of) 对于(for) 接口(interface) 类型别名(type)
//   异步(async) 等待(await)
// 符号:. 访问成员、[] 下标/取键、( ) 调用、=> 箭头、< > 泛型
//
// 期望输出:
// 数组映射=2,4,6
// 数组过滤=2
// 数组归约=6
// 字符串=你好世界 长度=4
// 字符串切片=世界
// Map取值=1
// Set去重=1,2,3
// JSON=小明,18
// Math=3
// 解构=1 2

// —— 数组:map / filter / reduce 都是英文方法名 ——
常量 数 = [1, 2, 3];
console.log("数组映射=" + 数.map((项) => 项 * 2).join(","));
console.log("数组过滤=" + 数.filter((项) => 项 % 2 === 0).join(","));
console.log("数组归约=" + 数.reduce((和, 项) => 和 + 项, 0));

// —— 字符串 ——
常量 名 = "你好世界";
console.log("字符串=" + 名 + " 长度=" + 名.length);      // length 数的是 UTF-16 码元,汉字算 1
console.log("字符串切片=" + 名.slice(2));

// —— Map:键值对(比对象更适合频繁增删)——
常量 表 = new Map<字符串, 数字>();
表.set("甲", 1);
表.set("乙", 2);
console.log("Map取值=" + 表.get("甲"));

// —— Set:自动去重 ——
常量 集 = new Set([1, 2, 2, 3, 3]);
console.log("Set去重=" + [...集].join(","));             // ... 展开成数组

// —— JSON ——
接口 人 { 名字: 字符串; 年龄: 数字 }
常量 对象: 人 = JSON.parse('{"名字":"小明","年龄":18}');
console.log("JSON=" + 对象.名字 + "," + 对象.年龄);

// —— Math ——
console.log("Math=" + Math.max(1, 3, 2));

// —— 解构:把数组/对象拆开 ——
常量 [甲, 乙] = [1, 2];
console.log("解构=" + 甲 + " " + 乙);

跑出来的结果:

text
数组映射=2,4,6
数组过滤=2
数组归约=6
字符串=你好世界 长度=4
字符串切片=世界
Map取值=1
Set去重=1,2,3
JSON=小明,18
Math=3
解构=1 2

讲解 ​

  • 数组.map(函数) 把每个元素变成新元素;filter 挑出满足条件的; reduce 把它们归成一个值。这三个是最常用的。
  • 字符串.length 数的是 UTF-16 码元(一个汉字算 1,一个 emoji 算 2)。
  • Map 用 .set / .get;Set 自动去重;JSON.parse 把字符串变成对象。
  • [...集] 用展开符号把 Set 摊成数组。

练习 ​

  1. 用 filter 挑出数组里的偶数并打印。
  2. 用 Map 存三个人的年龄,再打印其中一个人的。

第 9 课 异步 ​

本课关键字 ​

汉语英文干什么
异步async标在函数前,让它返回 Promise
等待await等一个 Promise 出结果
尝试 / 捕获 / 最终try / catch / finally异常处理
抛出throw抛异常
新建new新建 Error("…")

本课符号 ​

符号用法
.then(...)接住 Promise 的结果(方法名英文)
=>传给 .then 的函数
< >Promise<数字> —— Promise 里装什么类型

例子的真源码 ​

ts
// 第 9 课:异步
//
// 关键字(英文对照):异步(async) 等待(await) 函数(function) 返回(return)
//   新建(new) 数字(number) 字符串(string) 空型(void) 常量(const) 局部变量(let)
// 符号:=> 箭头、< > 泛型、.then 链式调用、() 调用
//
// 期望输出:
// 先打印这一行
// 等待得到=42
// 出错也接住了
// 两件事都做完了

// —— 异步函数:函数前面加 异步,它就返回一个 Promise ——
异步 函数 取数(): Promise<数字> {
  返回 42;
}

异步 函数 会失败(): Promise<数字> {
  抛出 new Error("故意失败");
}

异步 函数 主(): Promise<空型> {
  console.log("先打印这一行");

  常量 值 = 等待 取数();               // 等待 = await:等 Promise 出结果
  console.log("等待得到=" + 值);

  尝试 {                                // 尝试 = try
    等待 会失败();
  } 捕获 (错误) {                       // 捕获 = catch
    console.log("出错也接住了");
  }
}

// 主() 返回 Promise,用 .then 接住它(.then 是英文方法名)
主().then(() => {
  console.log("两件事都做完了");
});

跑出来的结果:

text
先打印这一行
等待得到=42
出错也接住了
两件事都做完了

讲解 ​

  • 异步 函数 里写 等待 某个Promise,代码读起来是同步的,实际是异步的。
  • .then 是 Promise 的方法(英文名);也可以用 等待 在另一个 异步 函数 里接。
  • 异步出错用 尝试 / 捕获 接(等待 抛出的异常能被 捕获 抓到)。

练习 ​

  1. 写一个 异步 函数 延时(毫秒: 数字),用 new Promise + setTimeout 实现。
  2. 用 尝试/捕获 接住一个失败的 等待。

附录 A 符号用法速查 ​

一句话:TypeScript 这边符号一个都没换。

符号名字典型写法说明
//行注释// 说明到行尾
/* */块注释/* 多行 */
;分号常量 甲 = 1;可省,但建议写
{ }花括号函数 甲() { … }包代码块;对象/名域/类体也用它
( )圆括号如果 (甲 > 乙) / 相加(1, 2)条件、参数、调用
[ ]方括号表[0] / 数字[]取下标(从 0)/ 数组类型
.点对象.名字访问成员
:冒号甲: 数字类型标注
=>箭头(甲) => 甲 + 1箭头函数
< >尖括号取大<数字>(…) / Promise<数字>泛型
|竖线字符串 | 未定义联合类型
&与号甲 & 乙交叉类型(同时具备)
?问号名字?: 字符串 / 甲 ? 乙 : 丙可选属性 / 三元
?.可选链配置.名字?.length空就短路成 undefined
??空值合并可能空 ?? "默认"只对 空值/undefined 生效
!感叹号甲! / !真值非空断言 / 逻辑非
...省略号[...表] / (...数: 数字[])展开 / 剩余参数
`反引号`你好,${名字}`模板串,${} 里放表达式
= += -=赋值甲 += 1
++ --自增自减甲++
=== !==严格相等/不等甲 === 乙推荐
== !=松散相等/不等甲 == 乙会做类型转换
&& ||与或真 && 假没有汉语替代拼写
+ - * / %算术甲 % 2+ 也拼接字符串

附录 B 关键字总表 ​

下表是从 docs\中文对照表.md 原样切进来的 —— 那张表由 tsc/cmd/genzhkeywords 从编译器真正在用的词表(zh_keywords.txt)自动生成, 并有判定脚本防它漂移。放进教程里是为了让你不用来回翻文件。

一、关键字(85 个) ​

中文英文中文英文
抽象abstract是类型is
访问器accessor取键keyof
任意any局部变量let
作为as模块module
断言assert名域namespace
断言式asserts永不never
异步async新建new
等待await空值null
大整数bigint数字number
布尔boolean对象object
跳出break遍历of
情形case输出out
捕获catch重写override
类型class软件包package
常量const私有private
构造器constructor受保护protected
跳过continue公开public
调试器debugger只读readonly
声明declare需要require
默认default返回return
推迟defer满足satisfies
删除delete设值set
执行do静态static
否则else字符串string
枚举enum父类super
导出export选择switch
继承extends符号symbol
假值false自身this
最终finally抛出throw
对于for真值true
来自from尝试try
函数function类型别名type
取值get取类型typeof
全局global未定义undefined
如果if唯一unique
立即immediate未知unknown
实现implements使用using
导入import变量var
属于in空型void
推断infer每当while
是实例instanceof伴随with
接口interface产出yield
内建intrinsic

那张表还有第二~六节:译名定则、这些仍然必须写英文、保留字副作用、 关键字当属性名、一个完整例子。见 docs\中文对照表.md。


附录 C 常见坑 ​

坑现象怎么办
单字关键字不存在写 类 编不过汉语关键字不少于两个字:class 是 类型,不是 类
想当然的译名写 超级、可空 报「找不到名字」按附录 B 的表写:super 是 父类,null 是 空值
未定义 当值不行已修(见下)值与类型两种位置都能写 未定义;空值(null) 同理
库的成员是英文写 表.尺寸 报「没有该属性」运行库的名字没汉化:用 表.length、console.log
模块路径写中文编译过但运行时找不到模块路径是字符串,必须原样:来自 "./数学"
选择 忘 跳出会串到下一个 情形每个 情形 末尾写 跳出
== 与 === 混用出现古怪的类型转换 bug一律用 === / !==
`` 当默认值
忘了 --rewriteRelativeImportExtensions显式写 "./数学.ts" 时产物路径不对推荐无扩展名导入("./数学"),见 tests-chts\proj\

案例:未定义 当值曾经不行 —— 现在修好了 ​

这条值得留着,因为它演示了「让汉语关键字真正等价于英文」会卡在哪些地方。

症状:常量 甲 = 未定义; 报 TS2304 找不到名字;而 : 未定义(当类型)完全正常。

根因:null / true / false 是 JS 的字面量,词法关键字直接变成字面量节点, 打印器再从种类取规范拼写 —— 所以 空值 与 null 完全等价。 但 undefined 在 JS 里不是字面量,而是全局对象上的一个名字: 上游把它按文字注册成全局符号(checker.go 里的 c.globals["undefined"]), 值位置也按普通标识符解析。于是汉语拼写两头落空:

  • 解析时查 c.globals["未定义"] → 查不到;
  • 产物又会把 未定义 原样打印成标识符,而 JS 里没有这个全局名 → 运行时 ReferenceError。

修法(两处,都落在边界上):

  1. 解析器:值位置把标识符文本归一化成规范拼写 undefined (parser.parseUndefinedKeywordExpression)—— 解析与类型一次解决;
  2. 打印器:当节点文本是 undefined 而源码拼写不是时,用节点文本而不是切源码 (printer.getTextOfNode)—— 否则汉语拼写照样进产物。

关键约束:只归一化【值位置】。 属性名与成员名是运行时的真实属性名,必须原样保留:

ts
常量 戊 = { 未定义: 1 };      // 属性名就叫「未定义」,产物里也是 未定义
console.log(戊.未定义);        // 成员名同理

同一类问题会在多个位置出现——修的时候要一个位置一个位置地扫: 值位置、类型位置、数组字面量、默认参数、模板串、以及对象简写属性 ({ 未定义 } 里「名字就是值引用」,也必须归一化,否则报 TS18004)。 上面这几种位置现在都验过,由 tests-chts\undefined.ts 靶子 + 判定第 11 节 永久钉住:它同时断言「值位置与简写属性是 undefined」与「属性名仍然是 未定义」。

顺带说:为什么要真编译 ​

这条边界不是读文档读出来的,是真编译撞出来的。 如果教程只是照着关键字表抄一遍、不实际编译运行,读者会抄到一段跑不起来的中文代码。 这也是为什么本教程的每个例子都由 tests-chts\judge_tutorial.ps1真编译、真运行、逐行核对输出,而且那个判据自带自测(改坏必须被抓)。


那,英文怎么办? ​

照样用,而且可以混着写。 例如:

ts
常量 表: Array<number> = [1, 2, 3];     // 汉语关键字 + 英文类型名
函数 加倍(值: number): number { 返回 值 * 2; }
console.log(表.map(加倍));

汉语不是替换英文,是并行多出来的一种拼写。 这也是为什么「随便拿一个开源 TypeScript 项目,用我们的编译器编,照样编得过」。

以 MIT / Apache-2.0 等宽松许可发布 · 联系作者 QQ:1396257961