TypeScript 入门:给 JavaScript 穿上类型的铠甲

从 JavaScript 到 TypeScript,理解类型系统如何让代码更安全、更易维护

15 分钟阅读
小明

TypeScript 入门:给 JavaScript 穿上类型的铠甲

你是不是也遇到过这种场景?

function greet(person) {
  return `Hello, ${person.name}!`
}

greet({ name: 'xiaoming' })  // ✅ Hello, xiaoming!
greet('xiaoming')             // ❌ undefined
greet(123)                    // ❌ undefined

代码跑起来才发现参数传错了,一个低级错误,却浪费了半小时调试。

如果能在写代码时就发现错误,该多好?

TypeScript 就是来解决这个问题的。


一、TypeScript 是什么?

1.1 官方定义

TypeScript is JavaScript with syntax for types.

TypeScript 是带类型语法的 JavaScript。

更直白地说:

TypeScript = JavaScript + 类型系统

1.2 为什么需要类型?

JavaScript 是动态类型语言,变量可以是任何类型:

let value = 'hello'  // 字符串
value = 123          // 变成数字
value = true         // 变成布尔值
value = {}           // 变成对象

这很灵活,但也很危险:

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

add(1, 2)      // 3 ✅
add('1', '2')  // '12' ❌ 预期是 3,实际是字符串拼接

类型系统的作用

  1. 提前发现错误
    在编译时就发现类型不匹配,而不是运行时才报错。
  2. 更好的代码提示
    编辑器知道变量类型,能提供精准的代码补全。
  3. 代码即文档
    类型本身就是最好的文档,不用猜参数是什么类型。
  4. 重构更安全
    修改一个函数签名,所有调用的地方都会报错提示。

1.3 TypeScript 的编译过程

TypeScript 代码
  ↓ tsc (TypeScript Compiler)
JavaScript 代码
  ↓ Node.js / 浏览器
运行

TypeScript 最终会编译成 JavaScript,所以可以运行在任何支持 JavaScript 的环境。


二、安装和配置

2.1 安装 TypeScript

# 全局安装
$ npm install -g typescript

# 项目安装(推荐)
$ npm install --save-dev typescript

# 查看版本
$ tsc --version

2.2 初始化项目

# 创建配置文件
$ tsc --init

会生成 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",           // 编译目标
    "module": "ESNext",           // 模块系统
    "strict": true,               // 严格模式
    "esModuleInterop": true,      // 兼容性
    "skipLibCheck": true,         // 跳过库文件检查
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

2.3 编译和运行

方式1:手动编译

# 编译单个文件
$ tsc index.ts

# 编译整个项目
$ tsc

# 监听模式
$ tsc --watch

方式2:ts-node(开发环境)

$ npm install --save-dev ts-node

# 直接运行 TypeScript
$ ts-node index.ts

方式3:Vite/Next.js(现代工具链)

现代前端框架都内置 TypeScript 支持,无需手动配置。


三、基础类型

3.1 原始类型

// 字符串
let name: string = 'xiaoming'

// 数字
let age: number = 25

// 布尔值
let isStudent: boolean = true

// null 和 undefined
let n: null = null
let u: undefined = undefined

// Symbol
let sym: symbol = Symbol('key')

// BigInt
let big: bigint = 100n

3.2 数组

// 方式1:类型[]
let numbers: number[] = [1, 2, 3]
let names: string[] = ['xiaoming', 'xiaohong']

// 方式2:Array<类型>
let numbers2: Array<number> = [1, 2, 3]

// 混合类型数组
let mixed: (number | string)[] = [1, 'hello', 2]

3.3 元组(Tuple)

元组是固定长度和类型的数组:

// 定义一个 [string, number] 的元组
let person: [string, number] = ['xiaoming', 25]

// ❌ 错误:类型不匹配
person = [25, 'xiaoming']

// ❌ 错误:长度不匹配
person = ['xiaoming', 25, true]

// 可选元素
let point: [number, number, number?] = [1, 2]

3.4 对象

// 定义对象类型
let user: {
  name: string
  age: number
  email?: string  // 可选属性
} = {
  name: 'xiaoming',
  age: 25
}

// 只读属性
let point: {
  readonly x: number
  readonly y: number
} = {
  x: 10,
  y: 20
}

// ❌ 错误:不能修改只读属性
point.x = 30

3.5 any 和 unknown

any:放弃类型检查

let value: any = 'hello'
value = 123
value = true
value.foo.bar  // 不会报错,但运行时可能出错

unknown:更安全的 any

let value: unknown = 'hello'

// ❌ 不能直接使用
console.log(value.length)

// ✅ 需要类型守卫
if (typeof value === 'string') {
  console.log(value.length)  // OK
}

建议:尽量用 unknown 而不是 any

3.6 void, never

void:没有返回值

function log(message: string): void {
  console.log(message)
  // 不return 或 return undefined
}

never:永远不会返回

// 抛出异常
function throwError(message: string): never {
  throw new Error(message)
}

// 死循环
function infiniteLoop(): never {
  while (true) {}
}

四、函数类型

4.1 函数声明

// 完整类型标注
function add(a: number, b: number): number {
  return a + b
}

// 箭头函数
const multiply = (a: number, b: number): number => a * b

// 可选参数
function greet(name: string, greeting?: string): string {
  return `${greeting || 'Hello'}, ${name}!`
}

// 默认参数
function greet2(name: string, greeting: string = 'Hello'): string {
  return `${greeting}, ${name}!`
}

// 剩余参数
function sum(...numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0)
}

4.2 函数类型

// 定义函数类型
type MathOperation = (a: number, b: number) => number

const add: MathOperation = (a, b) => a + b
const subtract: MathOperation = (a, b) => a - b

// 函数类型作为参数
function calculate(
  a: number,
  b: number,
  operation: MathOperation
): number {
  return operation(a, b)
}

calculate(10, 5, add)       // 15
calculate(10, 5, subtract)  // 5

4.3 函数重载

// 重载签名
function combine(a: string, b: string): string
function combine(a: number, b: number): number

// 实现签名
function combine(a: string | number, b: string | number): string | number {
  if (typeof a === 'string' && typeof b === 'string') {
    return a + b
  }
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b
  }
  throw new Error('Invalid arguments')
}

combine('hello', 'world')  // 'helloworld'
combine(1, 2)              // 3
// ❌ 错误
combine('hello', 123)

五、接口(Interface)

接口定义对象的结构:

interface User {
  name: string
  age: number
  email?: string          // 可选
  readonly id: number     // 只读
}

const user: User = {
  id: 1,
  name: 'xiaoming',
  age: 25
}

// ❌ 错误:缺少必填属性
const user2: User = {
  name: 'xiaohong'
}

// ❌ 错误:不能修改只读属性
user.id = 2

5.1 接口继承

interface Person {
  name: string
  age: number
}

interface Student extends Person {
  studentId: string
  grade: number
}

const student: Student = {
  name: 'xiaoming',
  age: 20,
  studentId: '2024001',
  grade: 2
}

5.2 索引签名

interface Dictionary {
  [key: string]: string
}

const dict: Dictionary = {
  hello: '你好',
  world: '世界'
}

dict.foo = 'bar'  // OK
dict.test = 123   // ❌ 错误:必须是 string

六、类型别名(Type)

typeinterface 很相似,但更灵活:

// 基本用法
type User = {
  name: string
  age: number
}

// 联合类型
type Status = 'pending' | 'success' | 'error'

// 交叉类型
type Student = User & {
  studentId: string
}

// 函数类型
type MathOp = (a: number, b: number) => number

// 泛型
type Result<T> = {
  data: T
  error: string | null
}

6.1 Type vs Interface

特性TypeInterface
定义对象
继承&extends
联合类型
声明合并

建议

  • 定义对象用 interface
  • 联合类型、元组用 type

七、联合类型和交叉类型

7.1 联合类型(Union)

// 或的关系
type Status = 'loading' | 'success' | 'error'

function showStatus(status: Status) {
  console.log(status)
}

showStatus('loading')   // ✅
showStatus('success')   // ✅
showStatus('pending')   // ❌ 错误

// 多种类型
type StringOrNumber = string | number

let value: StringOrNumber = 'hello'
value = 123  // OK

7.2 交叉类型(Intersection)

// 且的关系
type Person = {
  name: string
  age: number
}

type Employee = {
  employeeId: string
  department: string
}

type Staff = Person & Employee

const staff: Staff = {
  name: 'xiaoming',
  age: 25,
  employeeId: 'E001',
  department: 'IT'
}

八、泛型(Generics)

泛型让类型也可以参数化:

8.1 泛型函数

// 不用泛型:需要为每种类型写一个函数
function identityString(value: string): string {
  return value
}

function identityNumber(value: number): number {
  return value
}

// 使用泛型:一个函数搞定
function identity<T>(value: T): T {
  return value
}

identity<string>('hello')  // 'hello'
identity<number>(123)      // 123
identity(true)             // 类型推断为 boolean

8.2 泛型接口

interface Response<T> {
  data: T
  code: number
  message: string
}

// 使用
const userResponse: Response<User> = {
  data: { name: 'xiaoming', age: 25 },
  code: 200,
  message: 'success'
}

const listResponse: Response<User[]> = {
  data: [
    { name: 'xiaoming', age: 25 },
    { name: 'xiaohong', age: 23 }
  ],
  code: 200,
  message: 'success'
}

8.3 泛型约束

// 限制泛型必须有 length 属性
function getLength<T extends { length: number }>(value: T): number {
  return value.length
}

getLength('hello')        // 5
getLength([1, 2, 3])      // 3
getLength({ length: 10 }) // 10
// ❌ 错误
getLength(123)

九、实战案例

9.1 Axios 请求封装

import axios, { AxiosResponse } from 'axios'

// 定义响应结构
interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
}

// 定义用户类型
interface User {
  id: number
  name: string
  email: string
}

// 封装请求函数
async function fetchUser(id: number): Promise<User> {
  const response: AxiosResponse<ApiResponse<User>> = await axios.get(
    `/api/users/${id}`
  )
  
  if (response.data.code !== 200) {
    throw new Error(response.data.message)
  }
  
  return response.data.data
}

// 使用
const user = await fetchUser(1)
console.log(user.name)  // 类型安全,有代码提示

9.2 Vue 3 组件

<script setup lang="ts">
import { ref, computed } from 'vue'

interface Todo {
  id: number
  text: string
  completed: boolean
}

const todos = ref<Todo[]>([])

const unfinishedCount = computed(() => {
  return todos.value.filter(t => !t.completed).length
})

function addTodo(text: string) {
  todos.value.push({
    id: Date.now(),
    text,
    completed: false
  })
}

function toggleTodo(id: number) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) {
    todo.completed = !todo.completed
  }
}
</script>

9.3 React 组件

import React, { useState } from 'react'

interface Props {
  initialCount?: number
}

const Counter: React.FC<Props> = ({ initialCount = 0 }) => {
  const [count, setCount] = useState<number>(initialCount)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
    </div>
  )
}

十、常见问题

Q1: 什么时候用 any?

尽量不用!

但以下情况可以考虑:

  1. 三方库没有类型定义
  2. 动态数据结构(JSON.parse 的结果)
  3. 复杂到无法定义类型

更好的选择是用 unknown + 类型守卫。

Q2: interface 和 type 如何选择?

建议

  • 定义对象结构用 interface
  • 联合类型、元组用 type
  • 团队统一风格即可

Q3: 如何给三方库添加类型?

方式1:安装类型定义包

$ npm install --save-dev @types/lodash

方式2:手动声明

创建 types/lodash.d.ts

declare module 'lodash' {
  export function chunk(arr: any[], size: number): any[][]
}

Q4: 严格模式要开吗?

强烈建议开启!

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

虽然初期会有很多错误,但能避免大量运行时 bug。


十一、进阶技巧

11.1 类型守卫

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function process(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase())  // 类型收窄为 string
  }
}

11.2 工具类型

TypeScript 内置了很多工具类型:

interface User {
  id: number
  name: string
  email: string
  age: number
}

// Partial: 所有属性变可选
type PartialUser = Partial<User>
// { id?: number; name?: string; ... }

// Required: 所有属性变必填
type RequiredUser = Required<User>

// Pick: 选择部分属性
type UserBasic = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// Omit: 排除部分属性
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string; age: number }

// Readonly: 所有属性变只读
type ReadonlyUser = Readonly<User>

11.3 条件类型

type IsString<T> = T extends string ? true : false

type A = IsString<string>   // true
type B = IsString<number>   // false

// 实用示例:提取 Promise 的返回类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T

type A = UnwrapPromise<Promise<string>>  // string
type B = UnwrapPromise<number>           // number

十二、小明的建议

  1. 循序渐进
    从简单的类型标注开始,不要一上来就写复杂泛型。
  2. 善用类型推断
    TypeScript 很聪明,很多时候不需要手写类型:
    // ❌ 冗余
    const name: string = 'xiaoming'
    
    // ✅ 简洁
    const name = 'xiaoming'  // 推断为 string
    
  3. 开启严格模式
    虽然初期痛苦,但能避免大量 bug。
  4. 多看开源项目
    看 Vue、React 的类型定义,学习最佳实践。
  5. 工具很重要
    用 VS Code,开启 TypeScript 的所有检查。

总结

TypeScript 看起来复杂,但本质很简单:

给 JavaScript 加上类型检查。

好处是:

  • ✅ 提前发现错误
  • ✅ 更好的代码提示
  • ✅ 重构更安全
  • ✅ 代码即文档

学习路线

  1. 基础类型(number, string, boolean)
  2. 对象类型(interface, type)
  3. 函数类型
  4. 泛型
  5. 工具类型

掌握这些,日常开发就够用了。

高级技巧(条件类型、infer)可以慢慢学,不着急。


相关文章

有问题?欢迎评论区留言!


冷笑话时间 🎭

面试官:"为什么要用 TypeScript?"

程序员:"因为 JavaScript 太自由了。"

面试官:"自由不好吗?"

程序员:"自由到可以把字符串当数字加......"

面试官:"......录取。"

(完)