TypeScript 入门教程:01 — 为什么需要类型
1. 你以前怎么惨
你写了一个购物车计算函数。两个参数:商品单价和数量。
// shopping.jsfunctioncalcTotal(price,qty){returnprice*qty;}同事 A 从 API 拿到价格,是一个字符串"19.90"。他调用calcTotal("19.90", 3)。JS 把字符串悄悄转成数字,得到59.7。碰巧对了。
同事 B 从另一个接口拿到数量,也是字符串"5"。他调用calcTotal(12.5, "5")。又碰巧对了,JS 又帮你转了。
然后同事 C 来了。他在表单里取价格,用户不小心输入了"abc"。calcTotal("abc", 3)返回NaN。没有报错,没有红字,购物车总价显示 “NaN”。你翻了几百行代码才找到这一行。
更惨的是:同事 D 把参数顺序写反了——calcTotal(3, 19.90)。JS 照样算出一个数字,只是结果是错的。没人告诉你。
JS 的问题:它太"好心"了。什么东西都帮你悄悄转换,错了也不说。
2. 先看一眼烂代码
下面这些代码在 JS 里全部能跑。没有一个报错。结果全是错的。
// 数字 + 字符串 = 字符串拼接,不是加法"5"+3;// -> "53"(你想要 8)// 数学运算碰上字符串 = NaN,但 JS 不报错Math.max("hello","world");// -> NaN// 访问不存在的属性 = undefined,继续用 undefined 调用方法 = 炸constuser={name:"小明"};user.email.toUpperCase();// ❌ TypeError: Cannot read properties of undefined// 对象拼错字段名 = undefined,没人提醒你functiongetAge(person){returnperson.gae;// 拼错了 age ——返回 undefined}getAge({name:"张三",age:30});// -> undefined(应该是 30)// 数组方法用在字符串上——字符被当成元素遍历"hello".map(ch=>ch.toUpperCase());// ❌ TypeError: "hello".map is not a function你看出来了吗?每一个错误都是运行到那一行才炸。有些甚至不炸,只是悄悄地错了。你的测试覆盖率要 100% 才能抓到全部问题——现实中没有人做到。
3. 用新语法修好它
3.1 给参数贴标签:string、number、boolean
JS 的方式:参数叫name,但鬼知道它是什么。
functiongreet(name,age,isMember){return`${name}${age}${isMember}`;}greet(42,"小明","yes");// 全错,不报错TS 的方式:参数后面加冒号和类型名。就像快递包裹上贴标签。
functiongreet(name:string,age:number,isMember:boolean):string{return`${name}${age}${isMember}`;}greet(42,"小明","yes");// ❌ 编译就报错:第一个参数应该是 string类型标注就是给每个变量发一张身份证。身份证上的"姓名"栏只能写文字,"年龄"栏只能写数字。填错了,编译器直接拒绝。
三个基本类型:
: string— 文字。"hello"、"张三"。: number— 数字。整数、小数、NaN、Infinity都是 number。: boolean—true或false。没有第三种。
3.2 描述对象的形状:interface
JS 里对象是"麻袋"——什么东西都能往里塞。
functionprintUser(user){console.log(user.name,user.age);}// 下面三个调用都不报错,但只有第一个有意义:printUser({name:"小明",age:18});// 对的printUser({name:"小明"});// age = undefinedprintUser({nam:"小明",age:18,xxx:"垃圾"});// name 拼错 + 多余字段TS 用interface描述对象的"形状"——有哪些字段,各自什么类型。
interfaceUser{name:string;age:number;}functionprintUser(user:User):void{console.log(user.name,user.age);// TS 知道 user 一定有 name 和 age}printUser({name:"小明",age:18});// ✅// printUser({ name: "小明" }); // ❌ 缺 age// printUser({ name: "小明", age: 18, xxx: 1 }); // ❌ 多余字段interface就像一张表格模板。你定义了三个列:姓名、年龄、邮箱。任何人填表时必须每列都填,不能多填一列,也不能少填一列。
类比:interface= 快递单模板。"收件人"栏、"电话"栏、"地址"栏——缺一不可,不能多贴一张便利贴。
3.3 可选字段:?
现实中的表单不会每个字段都必填。邮箱可能没有,昵称可能不写。
interfaceProfile{name:string;// 必填email?:string;// 可选:有就填,没有就不填}functionshowProfile(p:Profile):string{// TS 强制你处理 email 可能是 undefined 的情况return`${p.name}(${p.email??"无邮箱"})`;}showProfile({name:"小明"});// ✅ 不填 email 也可以showProfile({name:"小红",email:"hi@x.com"});// ✅ 填了更好?= 表单上的"(选填)"标记。你不填它,值是undefined。TS 知道它可能是undefined,所以你不先判断就不能直接用(比如直接.toUpperCase()会报编译错)。
3.4 数组与字面量类型
数组:number[]表示"这个篮子里只能装苹果(数字)"。不能塞一根香蕉(字符串)。
functionsum(nums:number[]):number{returnnums.reduce((a,b)=>a+b,0);}sum([1,2,3]);// ✅// sum(["1", "2"]); // ❌ 字符串不能放进 number[]字面量类型:一个变量不只是string,它只能是"dark"或"light"。
// JS:theme 是 string,拼错了也不报错functionsetTheme(theme){if(theme==="dark"){/* ... */}}setTheme("drak");// 悄悄错了// TS:theme 只能是这两个值之一typeTheme="dark"|"light";functionsetTheme(theme:Theme):void{/* ... */}// setTheme("drak"); // ❌ 编译报错:"drak" 不在 "dark" | "light" 中|读作"或"。"dark" | "light"= 只能是 dark 或 light。
as const:像超级胶水,让 TS 把值当类型用,而不是宽泛的string/number。
constcfg={theme:"dark",port:3000}asconst;// cfg.theme 类型 = "dark"(不是 string)// cfg.port 类型 = 3000(不是 number)// 整个对象变成只读,不可修改4. 什么时候不该用
不要给一个 10 行的临时脚本加上全套interface。类型标注适合"会被多次调用"、“有其他人维护”、"数据流过多个函数"的场景。
- 写完就扔的脚本:别加类型,浪费时间。
- JSON 数据本身结构不明:比如解析一个你完全没见过的第三方 API 返回。先用
unknown而不是硬猜一个 interface。 as const不要用在"用户会修改"的数据上。配置常量用它,表单输入别用。
5. 小结
| 语法 | 一句话 | 示例 |
|---|---|---|
: string | 这个变量是文字 | name: string |
: number | 这个变量是数字 | age: number |
: boolean | 这个变量是 true/false | done: boolean |
interface | 描述对象长什么样 | interface User { name: string } |
? | 这个字段可以不填 | email?: string |
number[] | 数字组成的数组 | scores: number[] |
"a" | "b" | 值只能是这几个之一 | dir: "left" | "right" |
as const | 冻结值,让 TS 知道精确值 | { port: 3000 } as const |