导读:如果你写过一点 JavaScript,被
undefined的报错折磨过、被拼错的属性名坑过,那么这篇是给你的。我们不背语法表,而是把 TypeScript 想成一件事——给代码里的每样东西,提前贴一张写清楚「它是什么」的标签。读完你会知道类型到底替你挡了什么、any为什么是陷阱、以及为什么这些标签在代码真正跑起来之前就被撕掉了。零基础从头读;已经会一点的,直接跳到第六、第七节。
一、为什么要给 JavaScript 加类型?
JavaScript 很自由,自由到有点危险。它允许你把不搭界的东西凑在一起,也不吭声:
"5" - 1 // 结果是 4 —— JS 偷偷把字符串当数字减了
"5" + 1 // 结果是 "51" —— 同样两个值,这次它当字符串拼了
更常见的是这一类:
const 用户 = { 名字: "小初" };
console.log(用户.名子); // 你把「名字」拼成了「名子」
// JS 不报错,安静地给你一个 undefined
问题不在于「出错」——谁都会拼错字。问题在于 JavaScript 要等代码真的运行到这一行,才发现不对。而这一行,可能是用户在页面上点了某个按钮、走了某条分支才触发的。于是错误从你的键盘出发,一路躲过你,最后在用户的屏幕上炸开:Cannot read properties of undefined。
TypeScript 要解决的就是这个「太晚了」。它的价值用一句话说清:
把「运行时才炸」的错,提前到「你打字时」就变红。
它怎么做到的?靠给代码里的每样东西事先贴一张标签,说清楚它是什么。变量是个盒子,标签写「只能装数字」;你往里塞了字符串,那个盯着标签的质检员——也就是编辑器和编译器——当场就拦住你,红线画在出错的那一行,而不是等到上线。这篇文章剩下的部分,都是在讲这些标签怎么贴、贴在哪。
二、给变量贴第一张标签:基础类型
给变量贴标签,语法就是在名字后面加一个冒号,冒号后面写类型:
let 年龄: number = 18;
let 名字: string = "小初";
let 是会员: boolean = true;
number、string、boolean 是你每天都会用到的三张基础标签。贴上之后,塞错东西就当场报错:
let 年龄: number = "十八";
// ❌ 报错:不能把 string 赋给 number
年龄 = "老了";
// ❌ 报错:标签一旦贴上,这个盒子以后也只能装数字
这里有个新手常有的误会:以为每个变量都得手写冒号,很累。其实不用——如果你在声明的同时就赋了值,TypeScript 会自己看这个值、推断出标签:
let 名字 = "小初";
// 你没写冒号,但 TS 已经知道「名字」是 string
名字 = 42; // ❌ 照样报错
这叫类型推断。所以实践里你写的冒号比想象中少:初始值能说明问题的地方,交给 TS 自己看就行;只有当下暂时没值、或者想把标签写得比 TS 猜的更精确时,才手动标注。
数组也是一张标签,写法是「元素类型 + 一对方括号」:
let 分数: number[] = [90, 85, 100];
分数.push(88); // ✅ 数字,可以
分数.push("满分"); // ❌ 这个数组只装数字
标签不是负担,是让机器替你记住「这个盒子只装什么」——记性这种事,本来就不该靠人。
三、any:那张「随便装」的万能标签
TypeScript 里有一张特殊的标签,叫 any。贴上它,这个盒子就什么都能装、怎么用都不报错:
let 东西: any = 5;
东西 = "变成字符串"; // 不报错
东西 = true; // 不报错
东西.随便点个方法(); // 也不报错
听起来很方便,其实这是全篇你最该警惕的东西。any 的真正含义是:把标签撕了,告诉质检员「这个你别管」。于是第二节讲的那套保护,在这个盒子上全部失效——你又回到了纯 JavaScript,还白白多打了 : any 几个字。
那最后一行 东西.随便点个方法() 就是典型的坑:它编译时一声不吭,运行时照样炸给用户看。你花力气引入 TypeScript,就是为了不让这种事发生,而 any 恰好把这道防线亲手拆了。
你会在几个地方遇到它:接手一段老 JS 代码、用了某个没写类型的第三方库、或者单纯图省事。TypeScript 有时也会自己冒出 any(比如一个函数参数你没给它贴标签,它就默认是 any)。
any能让红线消失,但红线消失不等于错误消失——只是没人替你盯着了。把它当成「我暂时投降」的记号,越少越好。
四、给函数贴标签:进料口和出料口
函数像一台机器:参数是进料口,返回值是出料口,两头都能贴标签。进料口的标签写在参数后面,出料口的标签写在括号后面:
function 打招呼(名字: string): string {
return "你好," + 名字;
}
这行代码里,名字: string 是进料口的标签,): string 是出料口的标签。贴好之后,喂错料当场就被拦:
打招呼("小初"); // ✅
打招呼(42); // ❌ 进料口要 string,你给了 number
打招呼(); // ❌ 少喂了一个参数
出料口的标签也不是摆设。如果你在函数里手滑返回了不该返回的东西,它会当场报——相当于机器承诺「我出的是字符串」,你却让它吐了个数字,质检员不答应:
function 打招呼(名字: string): string {
return 名字.length; // ❌ 说好出 string,这里却要吐 number
}
如果某个参数可给可不给,在它后面加一个问号 ?,它就成了可选参数:
function 打招呼(名字: string, 称呼?: string): string {
return "你好," + (称呼 ? 称呼 : "") + 名字;
}
打招呼("小初"); // ✅ 不给称呼也行
打招呼("小初", "同学"); // ✅ 给了也行
给函数两头都贴好标签,别人调用它时不用翻你的源码——看标签就知道该喂什么、会吐什么。函数签名本身成了最新、最不会撒谎的文档。
五、描述一个对象长什么样:interface 与 type
前端代码里最常打交道的不是单个数字字符串,而是对象:接口返回的一条数据、一个组件收到的一堆参数,都是对象。那怎么给「一个对象」贴标签?用 interface,写一张「清单」,列清楚这个对象该有哪些字段、每个字段是什么类型:
interface 用户 {
名字: string;
年龄: number;
是会员: boolean;
}
有了这张清单,就能拿它当标签去贴:
const 小初: 用户 = {
名字: "小初",
年龄: 20,
是会员: true,
};
接下来,凡是对不上清单的,都会被逐条挑出来:
const 甲: 用户 = { 名字: "甲", 年龄: 20 };
// ❌ 缺了「是会员」这一格
const 乙: 用户 = { 名字: "乙", 年龄: "二十", 是会员: true };
// ❌「年龄」这一格要 number,你放了 string
const 丙: 用户 = { 名字: "丙", 年龄: 20, 是会员: true, 头像: "x.png" };
// ❌ 清单里没有「头像」这一格
如果某个字段可有可无,同样在它后面加问号:
interface 用户 {
名字: string;
年龄: number;
是会员: boolean;
头像?: string; // 有就填,没有也不报错
}
你可能还会看到另一种写法 type:
type 用户 = {
名字: string;
年龄: number;
};
在「描述一个对象的形状」这件事上,interface 和 type 几乎可以互换。它们有一些细微区别,但那是进阶话题——入门阶段你只要记住:两个都能用来描述对象,团队里统一一种就好。
这一节看起来简单,用处却极大。真实场景是这样的:后端接口返回 { id, title, status },你顺手给它写一个 interface,之后代码里但凡把 title 拼成 titel、或者以为 status 是数字,立刻就是一条红线,而不是等页面渲染出一片空白你再回来查半天。
interface是一张「这个盒子里必须有哪些格子、每格装什么」的清单,机器拿着它,逐格替你核对。
六、一个东西可能是好几种:联合类型与字面量类型
现实没那么整齐。有时一个值可能是好几种类型——比如一个 id,老系统里是数字,新系统里换成了字符串,两种你都得接得住。这时用一根竖线 | 把几张标签并起来,叫联合类型:
let 编号: string | number;
编号 = 42; // ✅
编号 = "abc"; // ✅
编号 = true; // ❌ 只允许 string 或 number
但联合类型有个配套规矩,这也是 TypeScript 特别聪明的地方:用它之前,你得先说清楚现在到底是哪一种。因为字符串能 .trim(),数字不能:
function 处理(编号: string | number) {
编号.trim();
// ❌ 报错:万一它是 number 呢?number 没有 trim
if (typeof 编号 === "string") {
编号.trim(); // ✅ 这里 TS 已经确定它是 string 了
}
}
这不是找你麻烦,而是逼你把「万一是另一种情况」当场想清楚——而 bug 最爱藏的地方,恰恰就是「我以为它一定是那种」。
标签还能精确到只能是某几个具体的值,这叫字面量类型:
let 状态: "成功" | "失败" | "加载中";
状态 = "加载中"; // ✅
状态 = "陈功"; // ❌ 拼错了,当场红,不在这三个之内
这个在前端里到处都是:一个请求的状态、一个按钮的尺寸、一个主题的名字,用字面量类型一钉死,拼错、写了没定义过的值,全都逃不掉。
最后还有一件 TypeScript 会盯着你的事:「可能没有值」本身也是一种类型。开了严格模式后,一个可能是 null 的东西,TS 不许你直接用,得先判过:
const 输入框 = document.querySelector("input");
// 页面上不一定真有这个元素,所以它的类型是 「元素 或 null」
输入框.value;
// ❌ 报错:万一没找到,null 上没有 value(这正是运行时会炸的那一步)
if (输入框) {
输入框.value; // ✅ 判过之后才让你用
}
联合类型让标签诚实地写出「它可能是这几种」,然后逼你在用之前把「到底是哪一种」搞清楚——把假设摊到明面上,bug 就没地方藏了。
七、类型是「打包时的标签」,不跟着货物发出去
最后这件事,很多人用了很久 TypeScript 都没意识到,但它能帮你想通一大堆疑惑:浏览器和 Node 其实根本不认识 TypeScript。
中间有一道你可能没太留意的工序,叫编译:一个叫 tsc 的工具(或者 Vite 这类构建工具顺手做掉),把你写的 .ts 变成 .js。而这一步,会把你贴的所有标签整个撕掉。对比一下前后:
// 你写的 .ts
function 打招呼(名字: string): string {
return "你好," + 名字;
}
// 编译出来、真正运行的 .js
function 打招呼(名字) {
return "你好," + 名字;
}
看到了吗?冒号后面的类型、返回值的标签,全没了。真正在浏览器里跑的,是右边这段干干净净的 JavaScript。
所以类型是「打包时贴、发货前撕」的标签:它在你开发、质检的阶段拦住错误;等货物(真正运行的 JS)出厂,标签一个字节都不带走。由此能推出两个结论,帮你避开常见误解:
- 类型不会让你的代码变快或变慢——它在运行时压根不存在,谈不上影响性能。
- 类型管得着「你写的代码」,管不着「运行时从外面进来的数据」——用户在输入框里敲的、网络接口返回的,都是货物已经上路之后才到的东西,那时标签早撕了。你标注
: 用户只是承诺它长这样,并不能保证后端真按这个发。
第二条尤其重要,也常常被误解成「我写了类型就万无一失了」。给「外面来的数据」也把好关,是另一套功夫,叫运行时校验——那是下一篇的事,这里你只要先记住这条边界在哪。
TypeScript 帮的是「写」的阶段,不是「跑」的阶段。它是写给你和你的编辑器看的备注,不是写给用户的浏览器看的。
八、小结
很多人第一印象里,TypeScript 是「更严格、更啰嗦的 JavaScript」,写起来束手束脚。但读到这里你应该能反过来看它了:
TypeScript 把「每样东西到底是什么」这件事,从你的脑子里、从注释里、从口头约定里,搬到了编译器能替你逐一核对的地方。你写的仍然是 JavaScript,只是给每样东西多贴了一张机器能读的标签。于是拼错的属性名、喂错的参数、忘了判的空,不再等到用户点进来才炸,而是在你打字的那一刻就变成一条红线。
| 你担心的问题 | 纯 JavaScript | TypeScript |
|---|---|---|
| 拼错属性名 / 参数喂错 | 运行到那行才炸,可能在用户端 | 打字时当场变红 |
| 一个值可能有好几种类型 | 全靠你自己记得判断 | 联合类型逼你用前先分清 |
| 可能是 null 的东西 | 忘了判空 → 运行时崩 | 不判过不让你用 |
| 函数该传什么、返回什么 | 翻源码或猜 | 看签名标签就知道 |
入门到这儿就够你上手了。你已经能看懂大部分 TypeScript 代码里那些冒号在说什么,也知道了 any 是把双刃剑、类型在运行时并不存在这两件不那么显然的事。剩下的进阶话题——泛型、工具类型、以及怎么给「外面来的数据」在运行时也把关——都建立在这套「贴标签」的直觉之上。
别把 TypeScript 当成更凶的 JavaScript。它只是把你本来就默认在心里的那些约定,写成机器能读、能替你当场检查的标签而已。

没有回应