从JS 到 TS:既生J,何生T?

5 阅读4分钟

在前端开发的时候,JavaScript作为弱类型语言还是太不抗压了,不能随便写的情况下它能让你随便写就离谱。今天我们谈谈其超集——TypeScript,它不仅为 JavaScript 增加了类型系统,还提供了很多开发工具支持。

为什么需要 TypeScript?

JavaScript 作为一门动态类型语言,虽然灵活,但在大型项目中容易出现类型相关的错误。TypeScript 作为 JavaScript 的超集,添加了静态类型检查,能够在编译阶段发现潜在问题,提高代码质量和开发效率。

// App.tsx
import { useState } from 'react'
import './App.css'
import HelloComponent from './components/HelloComponent.tsx'

function App() {
  // 基本类型声明
  let count: number = 10;
  const title: string = 'hello ts'
  const isDone: boolean = true
  const list: number[] = [1, 2, 3]
  
  // 元组类型
  const tuple: [number, string] = [1, '2']
  
  // 枚举类型
  enum Status {
    Pending,
    Fullfilled,
    Rejected
  }
  const pStatus: Status = Status.Pending
  
  // 对象类型约束 - 使用接口
  interface User {
    name: string;
    age: number;
    isSingle?: boolean;
  }
  const user: User = {
    name: 'zhangsan',
    age: 18,
    isSingle: true
  }
  
  return (
    <>
      {count}
      {title}
      {Status.Fullfilled}
      {user.name}
      <HelloComponent name='zhangsan' age={18} />
    </>
  )
}

export default App

通过类型声明,我们可以在编码阶段,就发现类型错误,js就要编译阶段了,也就是代码运行之后,比如将字符串赋值给 number 类型的变量等。大红大红的还挺好看的。

React 组件中的 TypeScript

在 React 也是疯狂支持TypeScript:

// HelloComponent.tsx
import React from 'react'

interface HelloComponentProps {
  name: string;
  age?: number; // 可选属性
}

const HelloComponent: React.FC<HelloComponentProps> = (props) => {
  return (
    <h2>hello user: {props.name}, age: {props.age}</h2>
  )
}

export default HelloComponent

使用 React.FC<Props> 泛型来约束函数组件,确保组件接收正确的 props 类型。

泛型?之前了解的时候印象里面好像还有任意类型的泛型 是的,TypeScript 中有任意类型的泛型。以下是几种常见的方式:

  1. 使用 any 类型
interface HelloComponentProps {
  name: string;
  age?: number;
  [key: string]: any; // 允许任意额外属性
}

const HelloComponent: React.FC<HelloComponentProps> = (props) => {
  return (
    <h2>hello user:{props.name}, age:{props.age}</h2>
  )
}
  1. 使用 unknown 类型(更安全)
interface HelloComponentProps {
  name: string;
  age?: number;
  extraData?: unknown; // 任意类型但需要类型检查
}
  1. 使用泛型参数
interface HelloComponentProps<T = any> {
  name: string;
  age?: number;
  extraData?: T; // 可以是任意类型
}

const HelloComponent: React.FC<HelloComponentProps> = (props) => {
  return (
    <h2>hello user:{props.name}, age:{props.age}</h2>
  )
}
  1. 使用联合类型
interface HelloComponentProps {
  name: string;
  age?: number;
  value?: string | number | boolean | object; // 支持多种特定类型
}

其中 any 类型是最宽松的,允许任何类型的值,但会失去类型检查的优势。在实际开发中,建议优先使用更具体的类型或 unknown 来保证类型安全。

为什么unkonwn更安全,简单来说:就是说我虽然能用unknown传入任意值,但是我不能乱用。比如传入是number但不能作为string来用。any就没这方面的烦恼了。

状态管理和事件处理

在状态管理方面,TypeScript 同样提供了强大的支持。特别是在处理表单和事件时,类型约束能够有效避免运行时错误:

// NameEditComponent.tsx
import React from 'react'

interface Props {
  userName: string;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const NameEditComponent: React.FC<Props> = (props) => {
  return (
    <>
      <label>Update name</label>
      <input value={props.userName} onChange={props.onChange} />
    </>
  )
}

export default NameEditComponent

通过约束 onChange 事件处理器的参数类型,我们确保了事件对象的正确使用。

除了<HTMLInputElement>还有一些其他常见的 HTML 元素类型例子:

// 1. 文本域 (textarea) 元素
const handleTextAreaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
  console.log(event.target.value);
};

<textarea onChange={handleTextAreaChange} />
// 2. 选择框 (select) 元素
const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
  console.log(event.target.value);
};

<select onChange={handleSelectChange}>
  <option value="1">选项1</option>
  <option value="2">选项2</option>
</select>
// 3. 按钮 (button) 元素
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  console.log('按钮被点击');
};

<button onClick={handleButtonClick}>点击我</button>
// 4. 输入框 (input) 元素 - 特定于不同类型的输入
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log(event.target.checked); // checkbox 使用 checked 而不是 value
};

<input 
  type="checkbox" 
  onChange={handleCheckboxChange} 
/>
// 5. 表单 (form) 元素
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault(); // 阻止默认提交行为
  console.log('表单提交');
};

<form onSubmit={handleFormSubmit}>
  {/* 表单内容 */}
</form>
// 6. div 或其他 HTML 元素
const handleClickDiv = (event: React.MouseEvent<HTMLDivElement>) => {
  console.log('div 被点击');
};

<div onClick={handleClickDiv}>点击这个 div</div>

重点说明:

  • ChangeEvent 通常用于表单元素的值变化事件(如 input, textarea, select)
  • MouseEvent 用于鼠标相关事件(如 click, mouseover 等)
  • FormEvent 专门用于表单事件(如 submit)
  • 每种 HTML 元素都有对应的类型定义,如 HTMLInputElementHTMLTextAreaElement
  • 这些类型帮助 TypeScript 理解事件对象中 target 的具体属性和方法

当然,你如果嫌烦,也可以直接不写(大红但不影响运行)或者any(直接就是回到js)

在 App 中使用组件

将这些组件整合到主应用中,我们可以看到完整的类型安全流程:

// App.tsx
import { useState } from 'react'
import './App.css'
import NameEditComponent from './components/NameEditComponent'

function App() {
  // 使用 TypeScript 约束 state 类型
  const [name, setName] = useState<string>('initialName')
  
  // 约束事件处理函数参数类型
  const setUsernameState = (event: React.ChangeEvent<HTMLInputElement>) => {
    setName(event.target.value)
  }
  
  return (
    <>
      <NameEditComponent userName={name} onChange={setUsernameState} />
    </>
  )
}

export default App

useState<string> 明确指定了状态的类型,使得后续对状态的操作都有类型安全保障。

小结

虽然 TypeScript 在初期需要写更多的类型声明代码,但这些投入会在项目规模增大时带来显著回报。它不仅能减少 bug,还能提高开发效率和代码质量。特别是在团队协作中,良好的类型约束能够大大降低沟通成本。

虽然即使 TypeScript 报错,只要 JavaScript 能运行,代码仍然可以执行。但还是建议在开发阶段解决所有类型错误。