TypeScript 入门基础:与原生 JavaScript 的详细对比
TypeScript 入门基础:与原生 JavaScript 的详细对比
前言
TypeScript 是由微软开发并维护的开源编程语言,它是 JavaScript 的一个超集。这意味着所有合法的 JavaScript 代码,同时也是合法的 TypeScript 代码。TypeScript 在 JavaScript 的基础上添加了可选的静态类型检查,让我们在开发阶段就能发现大量潜在错误,并获得更强大的编辑器智能提示与重构能力。
本文将带你从零开始学习 TypeScript 的核心基础,并在每个概念上对比原生 JavaScript,帮助你更直观地理解 TS 带来的价值。
1. 环境搭建与编译
JavaScript 的运行方式
原生 JS 可以直接在浏览器或 Node.js 中运行,无需任何编译过程。你写好一个.js文件,直接用 Node 执行或引入 HTML 即可。
TypeScript 需要编译
TypeScript 代码不能直接在浏览器或 Node.js 中运行,需要先通过TypeScript 编译器 (tsc)将其转换成 JS 代码。
安装 TypeScript:
npminstall-gtypescript编写一个简单的hello.ts:
functiongreet(name:string):string{return`Hello,${name}`;}console.log(greet("World"));编译并运行:
tsc hello.ts# 生成 hello.jsnodehello.js# 输出: Hello, Worldtsconfig.json 配置文件
实际项目中,我们通过tsconfig.json精细控制编译行为:
{"compilerOptions":{"target":"ES2020",// 编译目标 JS 版本"module":"commonjs",// 模块系统"strict":true,// 开启所有严格类型检查"outDir":"./dist","rootDir":"./src"}}之后只需执行tsc即可编译整个项目。
对比总结:
- JS 零配置直接运行,开发轻快但缺乏编译时检查。
- TS 增加了编译环节,但换来的是类型安全和更早的错误发现。
2. 类型注解与类型推断
JavaScript 是一门动态类型语言:
// JS: 变量可以随时赋值为任意类型letmessage="Hello";message=42;// 完全合法,但可能引发运行时 BUGTypeScript 允许我们为变量添加类型注解:
// TS: 指定类型后,赋值其他类型会编译报错letmessage:string="Hello";message=42;// ❌ 错误: 不能将类型“number”分配给类型“string”。基本类型
TS 支持所有 JS 原始类型,并额外增加了一些:
| 类型 | 说明 | 示例 |
|---|---|---|
string | 字符串 | let name: string = "Alice"; |
number | 数字 | let age: number = 30; |
boolean | 布尔值 | let done: boolean = false; |
array | 数组 | let list: number[] = [1,2,3]; |
tuple | 固定长度、类型的元组 | let x: [string, number]; |
enum | 枚举 | (后面详谈) |
any | 任意类型,跳过检查 | let data: any = 5; |
void | 无返回值(函数) | function log(): void {} |
null/undefined | 与 strictNullChecks 相关 | |
never | 永远不存在的值的类型 | 函数抛出异常或死循环 |
object | 非原始类型 | let obj: object = {}; |
类型推断
TS 很智能,能自动推断变量的类型,不必处处标注:
letgreeting="Hi";// 推断为 stringgreeting=100;// ❌ 报错,因为已经被推断为 string这种情况与显式注解let greeting: string = "Hi"等价。
联合类型与类型别名
在 JS 中,一个函数参数既可以接收字符串也可以接收数字,通常只能在代码内判断:
// JS: 运行时判断functionprintId(id){if(typeofid==="string")console.log(id.toUpperCase());elseconsole.log(id);}TS 可以使用联合类型清晰表达:
typeID=string|number;// 类型别名functionprintId(id:ID):void{if(typeofid==="string"){console.log(id.toUpperCase());}else{console.log(id);}}string | number表示既可以是 string 也可以是 number,编辑器会根据类型缩小(Narrowing)提供准确的提示。
对比总结:
- JS 的类型信息存在于运行时和开发者的脑海中,容易遗忘导致类型错误。
- TS 的类型注解和推断将类型信息写入代码,成为“活文档”,在编码阶段就杜绝了类型不匹配的错误。
3. 接口(Interface)
JavaScript 对象是无固定形状的,函数只能靠文档或运行时的typeof去保证参数结构:
// JS: 期望 user 有 name 和 age 属性,但没有任何强制functiongetUserInfo(user){return`${user.name}is${user.age}years old`;}getUserInfo({name:"Bob"});// 运行时访问 undefined,可能造成 bugTypeScript 使用接口来定义对象的结构:
interfaceUser{name:string;age:number;}functiongetUserInfo(user:User):string{return`${user.name}is${user.age}years old`;}getUserInfo({name:"Bob"});// ❌ 编译错误:缺少 age 属性接口的高级特性
- 可选属性:
age?: number - 只读属性:
readonly id: number - 函数类型接口:
interfaceSearchFunc{(source:string,subString:string):boolean;} - 可索引类型:
[index: number]: string - 接口继承:
interfaceAdminextendsUser{role:string;} - 类实现接口:
classPersonimplementsUser{name:string;age:number;constructor(name:string,age:number){this.name=name;this.age=age;}}
对比总结:
- JS 中,对象的结构约束全靠约定和运行时检查,代码编写者需要自己保证一致性。
- TS 的接口在编译阶段强制检查对象形状,让代码更加自文档化,也让重构变得更加安全和容易。
4. 类(Class)
ES6 为 JS 引入了类语法,但缺少访问控制符(如private)和抽象类等特性:
// JS (ES6+)classAnimal{constructor(name){this.name=name;// 没有私有化机制,约定用 _name 表示私有}speak(){console.log(`${this.name}makes a noise.`);}}TypeScript 提供了完整的面向对象增强:
访问修饰符
public(默认):可自由访问private:只能在类内部访问protected:能在类及子类中访问
classAnimal{privatename:string;// 私有属性constructor(name:string){this.name=name;}publicspeak():void{console.log(`${this.name}makes a noise.`);}}constdog=newAnimal("Dog");dog.name;// ❌ 错误: 属性“name”为私有属性,只能在类“Animal”中访问。参数属性
TS 允许在构造函数参数中直接声明并初始化成员,简化代码:
classAnimal{constructor(privatename:string){}// 等价于声明了一个 private name 并在构造函数中赋值}抽象类
不能直接实例化的类,用于定义公共行为并由子类实现:
abstractclassAnimal{abstractmakeSound():void;// 抽象方法,子类必须实现move():void{console.log("moving...");}}classDogextendsAnimal{makeSound(){console.log("Woof!");}}与 JS 私有字段 (#) 对比
ES2022 引入了真正的私有字段#name,但 TS 的private只在编译时检查,编译到 JS 后依然可访问(除非目标 ES2022 并使用#)。通常 TS 项目中更常用private修饰符。
对比总结:
- JS 的类更简单,缺少访问控制和抽象机制,大型项目维护时容易误操作对象内部状态。
- TS 的类通过访问修饰符和抽象类提供了更严谨的 OOP 设计,提高封装性和代码的可维护性。
5. 函数
JavaScript 函数参数无类型约束,重载需要手动处理:
// JS: 模拟重载functionadd(a,b){if(typeofa==="number"&&typeofb==="number"){returna+b;}if(typeofa==="string"&&typeofb==="string"){returna.concat(b);}}TypeScript 函数可以定义参数和返回值的类型,还支持函数重载:
基本类型声明
functionadd(a:number,b:number):number;functionadd(a:string,b:string):string;functionadd(a:any,b:any):any{returna+b;}add(1,2);// 3add("a","b");// "ab"add(1,"b");// ❌ 报错:没有与此调用匹配的重载重载签名列表 + 一个实现函数,外部调用时只能使用已定义的重载签名,保证了类型安全。
可选参数和默认参数
functionbuildName(firstName:string,lastName?:string):string{if(lastName)return`${firstName}${lastName}`;returnfirstName;}// 或直接默认值:lastName: string = "Smith"剩余参数
functionsum(...numbers:number[]):number{returnnumbers.reduce((acc,cur)=>acc+cur,0);}对比总结:
- JS 中函数参数数量和类型完全由调用方决定,类型错误要到运行时才暴露。
- TS 通过类型注解、重载声明,使得函数调用契约一目了然,编译器会帮你检查传入的参数。
6. 泛型(Generics)
在 JavaScript 中,要想写一个能返回任意类型输入的函数,只能使用any或放弃类型:
// JS: 丢失了类型信息functionidentity(arg){returnarg;}letoutput=identity("hello");// output 是 any,没有类型提示TypeScript 的泛型允许我们在定义时使用类型变量,调用时才确定具体类型:
functionidentity<T>(arg:T):T{returnarg;}letoutput1=identity<string>("hello");// output1 明确为 stringletoutput2=identity(42);// 类型推断为 number泛型不仅适用于函数,还可以用于接口和类:
interfaceGenericRepository<T>{getById(id:number):T;getAll():T[];}classUserRepositoryimplementsGenericRepository<User>{getById(id:number):User{/* ... */}getAll():User[]{/* ... */}}泛型约束
可以限制泛型必须具有某些属性:
interfaceLengthwise{length:number;}functionlogLength<TextendsLengthwise>(arg:T):T{console.log(arg.length);returnarg;}logLength("hello");// OKlogLength(123);// ❌ number 没有 length 属性对比总结:
- JS 无法表达“某个函数可以处理任意类型但要保持类型关系”,只能依赖
any和手动类型判断。 - TS 的泛型让我们写出既灵活又类型安全的代码,并且不丢失类型信息,编辑器能够给出准确提示。
7. 枚举(Enum)
JavaScript 没有枚举类型,通常使用常量或对象模拟:
// JS 模拟枚举constDirection={Up:0,Down:1,Left:2,Right:3};这种方式没有类型约束,任意数字都能赋值,容易出错。
TypeScript 内置枚举:
enumDirection{Up,// 默认 0Down,// 1Left,// 2Right// 3}functionmove(dir:Direction):void{// ...}move(Direction.Up);// OKmove(5);// ❌ 错误:类型“5”的参数不能赋给“Direction”字符串枚举
enumStatus{Active="ACTIVE",Inactive="INACTIVE"}常量枚举
用const enum定义,编译时直接内联值,不生成额外的对象代码:
constenumColor{Red,Green,Blue}letc=Color.Red;// 编译后变成 let c = 0;对比总结:
- JS 中模拟枚举容易导致误用,且缺乏命名空间的聚合感。
- TS 枚举提供了有名字的常量集合,使得代码可读性更强,同时受类型系统的约束,避免无效值。
8. 高级类型与工具类型(概览)
随着应用规模扩大,你还会接触到更强大的类型工具,这些都是 JavaScript 完全不具备的:
- 交叉类型:
A & B同时满足多个类型 - 类型守卫:
typeof、instanceof、自定义判断,帮助类型缩小 - 类型断言:
as告诉编译器“我知道这是什么类型” - 非空断言:
!排除null/undefined - 工具类型:TypeScript 内置
Partial<T>、Required<T>、Readonly<T>、Pick<T, K>、Omit<T, K>等,快速基于已有类型创建新类型。
interfaceTodo{title:string;description:string;completed:boolean;}typeTodoPreview=Pick<Todo,"title"|"completed">;// { title: string; completed: boolean }这些特性在 JS 中只能靠人工保证,无法在编码阶段自动化验证。
9. 模块与命名空间
ES6 提供了标准的模块系统(import/export),TypeScript 完全兼容,并扩展了类型导出。
// user.tsexportinterfaceUser{name:string;age:number;}exportfunctioncreateUser(name:string,age:number):User{return{name,age};}// main.tsimport{User,createUser}from"./user";constuser:User=createUser("Alice",30);TS 还保留了“命名空间”(内部模块)的概念,用于组织代码,避免全局污染:
namespaceUtils{exportfunctionlog(msg:string):void{console.log(msg);}}Utils.log("Hi");不过现代项目更推荐使用 ES 模块。
对比总结:
- JS 使用 ES 模块,导入导出的是值,没有类型概念。
- TS 的模块可以导入导出类型(
interface、type等),这些在编译后会被完全擦除,只影响编译阶段,让跨文件的类型检查成为可能。
10. TypeScript 与 JavaScript 的互操作
实际项目中,我们常常需要与纯 JS 代码或第三方库协作。TS 为此提供了多种手段:
- 声明文件(
.d.ts):为 JS 库提供类型描述,例如jquery.d.ts。社区维护的 DefinitelyTyped 提供了海量@types包,安装即可:npm install --save-dev @types/lodash。 any和unknown:当无法确定类型时,any完全跳过检查,unknown要求使用前做类型判断,更安全。- 类型断言:
const el = document.getElementById("app") as HTMLDivElement; - 非空断言:
user!.name告诉编译器user一定不为null/undefined。
从 JS 迁移到 TS,可以渐进式进行:将.js重命名为.ts,逐步添加类型注解,开启strict模式将不安全的代码暴露出来并修复。
对比总结:
- JS 生态的海量库可以直接在 TS 中使用,TS 通过声明文件赋予了类型智能,而 JS 调用这些库时依然缺乏类型保障。
结语
TypeScript 并不是要替代 JavaScript,而是给 JavaScript 插上了“类型之翼”。它让我们在开发大型应用时,能够:
- 在编码阶段避免常见的类型错误
- 获得精确的智能提示和重构能力
- 写出自解释、可维护的代码
JavaScript 的灵活性依然在,但 TypeScript 让这种灵活性被约束在一个安全的边界内。正如前端框架之于原生 DOM,TypeScript 之于 JavaScript 也是开发效率的巨大提升。
如果你已经掌握了 JavaScript,那么学习 TypeScript 的成本并不高——它只是 JS 的**“类型注释版”**。希望本文的对比讲解能帮你迈出坚实的第一步,快速将 TS 应用到你的项目中。
