眼下使用 TypeScript 开发 React 应用越来越流行。用 TypeScript 写业务代码可以让代码更健壮,使用 TypeScript 开发的组件、库可以享受自动完成的体验。怪不得 Vue 3 升级的一大原因就是新版对 TypeScript 支持得更好。然而 TypeScript 问题博大精深,问题也千奇百怪。本文的问题是我在使用 TypeScript 是遇到的比较棘手的问题,如果你正在解决同样的问题,希望对你有点帮助。
方法重载
方法重载是有顺序的,不能随意放置,不要把更通用的重载放在更具体的重载之前:
- 代码
tsx
function fn(x: unknown): unknown;
function fn(x: HTMLElement): number;
function fn(x: HTMLInputElement): string;
const myElem: HTMLInputElement = new HTMLInputElement();
const value = fn(myElem);
- 错误
value
的类型:unknown
,不是预期的string
- 原因
TypeScript在解析函数调用时选择第一个匹配的重载。当前面的重载比后面的重载"更通用"时,后面的重载实际上是隐藏的,不能被调用。
- 解决办法
对重载进行排序,将更通用的签名放在更具体的签名之后:
tsx
declare function fn(x: HTMLInputElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: unknown): unknown;
const myElem: HTMLInputElement = new HTMLInputElement();
const value = fn(myElem);
索引签名
实际应用中,会遇到遍历一个对象键值的情况,那该如何定义对象接口类型呢?
- 代码
tsx
interface Book {
name: string;
author: string;
url: string;
}
const book: Book = {
name: 'React Hooks Guide',
author: 'xiaoming',
url: 'http://www.example.com',
};
let list: JSX.Element[] = [];
for (const key in book) {
const item = book[key];
// 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "Book"。
// 在类型 "Book" 上找不到具有类型为 "string" 的参数的索引签名。
list.push(
<li key={key}>
{key}: {item}
</li>
);
}
索引签名便于将值分配给对象,但不完全是类型安全的。它们表明无论访问哪个属性,对象都应该返回一个值。
更好的办法是遍历对象 entries
的属性来获取键值:
tsx
Object.entries(book).forEach((key, value) => {
list.push(
<li key={value}>
{key}: {value}
</li>
);
});
- 错误
元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "Book"。 在类型 "Book" 上找不到具有类型为 "string" 的参数的索引签名。
- 原因
这里通过 for in
语法遍历一个对象 book
, key
是变量导致值 book[key]
的不确定, TypeScript
无法推断其正确类型,只好提示隐式具有 "any" 类型
- 解决办法
使用索引签名,明确告知 Book
接口的对象 book
可以接受任何键,并在该键下返回特定类型。如下修改 Book
接口的定义:
tsx
interface Book {
[key: string]: string;
}
元组
学习 Vue3 的组合式函数时,我自定义了一个这样的组合式函数:
- 代码
tsx
// message.ts
import { ref } from 'vue'
export function useMessage(initValue: string) {
const message = ref(initValue)
function updateMessage(value: string) {
message.value = value
}
return [message, updateMessage]
}
然后在 index.vue
中使用 useMessage
:
html
// index.vue
<script lang="ts" setup>
import { useMessage } from './message'
const [message, updateMessage] = useMessage('Hello')
</script>
<template>
<button
class="btn"
@click="updateMessage('world')"
>
Change
</button>
<div>{{ message }}</div>
</template>
先前没使用 TypeScript,啥问题都没有,加了第 10 行的 updateMessage
函数报了下面的类型错误:
- 错误
此表达式不可调用。
"Ref<string> | ((value: string) => void)"
类型的部分要素不可调用。
- 原因
TypeScript 将元组类型视为比可变长度数组类型更具体。这意味着可变长度数组类型不能分配给元组类型。
这里,尽管我们作为人类可能会将返回值视为具有 [Ref<string>, (value: string) => void]
内容,但 TypeScript 推断它是更一般的 (Ref<string>| (value: string) => void)[]
数组类型:
- 解决办法
其实这不是 Vue3 的 Bug,我没按 Vue3 的套路来,组合式函数返回值应该使用对象,而不是元组。
如果已经习惯 React Hooks,确实要使用元组方式,可以使用显式元组类型,而不是 TypeScript 推断的更一般的数组类型。
tsx
// message.ts
import { ref } from 'vue'
export function useMessage(initValue: string) {
// ...
return [message, updateMessage] as const
}
上述代码中,对返回值使用元组类型断言,也是可以解决问题的。
类型断言
类型断言只能够"欺骗"TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
tsx
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
- 错误
上面的例子编译时不会报错,但在运行时会报错:
Uncaught TypeError: animal.swim is not a function
- 原因
(animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。
可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。
- 解决办法
使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
泛型箭头函数
编译扩展名为 tsx
的文件时,泛型箭头函数的语法与 JSX 语法冲突:
tsx
// sample.tsx
const identity = <T>(input: T) => input;
- 错误
JSX 元素"T"没有相应的结束标记。
- 原因
在 .tsx 文件中,尝试为箭头函数编写类型参数 <T>
会导致语法错误,因为没有为开放的 T
元素提供结束标记。
- 解决办法
可以在类型参数中添加 = unknown
约束。类型参数默认为 unknown
类型,因此这根本不会更改代码行为。它只是指示 TypeScript 读取类型参数,而不是 JSX 元素:
tsx
const identity = <T = unknown>(input: T) => input;
HOC
下面是一个高阶函数的示例:
- 代码
tsx
const withPersistentData = (key: string) => (WrappedComponent) => {
return class extends Component {
state: Persistent = { data: '' };
componentDidMount() {
const data = localStorage.getItem(key);
this.setState({ data });
}
render() {
return (
<WrappedComponent
data={this.state.data}
{...this.props}
/>
);
}
};
};
const App = (props: Persistent) => {
return <div>{props.data}</div>;
};
const app = withPersistentData('name')(App);
export default app;
- 错误
参数"WrappedComponent"隐式具有"any"类型。
- 原因
TypeScript 类型系统推断不出 WrappedComponent
参数的类型,需要手动添加类型注解。
- 解决办法
为 WrappedComponent
参数添加 FC<State>
类型
tsx
const withPersistentData = (key: string) => (WrappedComponent: FC<State>) => {
// ...
}
MouseEvent
- 代码
tsx
import { useState } from 'react';
import styles from './App.module.css';
function App() {
const [point, setPoint] = useState({ x: 0, y: 0 });
const handleMove = (e: MouseEvent) => {
setPoint({ x: e.clientX, y: e.clientY });
};
return (
<div
className={styles.container}
onMouseMove={handleMove}
>
{point.x}, {point.y}
</div>
);
}
export default App;
- 错误
不能将类型"(e: MouseEvent) => void"分配给类型"MouseEventHandler"。
- 原因
将 HTML MouseEvent 事件类型和 React MouseEvent 事件类型搞混淆了,此处应该使用 React MouseEvent 事件类型。
- 解决办法
从 React 包 导入 MouseEvent:
tsx
import { MouseEvent } from 'react';
RouteRecordRaw
使用 Vite vue-router 做了个文件系统功能:
tsx
const routes = [
{
path: '/',
name: 'home',
component: HomeView
}
]
const modulesFiles = import.meta.glob('../views/**/*App.vue')
const map = new Map()
for (const module in modulesFiles) {
// ...
if (paths?.length === 1) {
routes.push({
path: '/' + path,
name: path,
component: modulesFiles[module],
children: []
})
}
// ...
}
结果 component
报 TypeScript 类型报错。
- 错误
不能将类型"() => Promise"分配给类型"DefineComponent<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<ExtractPropTypes<{}>>, {}, {}>"。
DefineComponent 之类的类型匹配问题通常是没有正确使用第三方库的 TypeScript 类型造成的。
- 原因
代码没有显式定义 routes 类型
- 解决办法
将 routes
定义为 vue-router
内置的 RouteRecordRaw 类型:
tsx
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: HomeView,
children: []
}
]
RouteRecordSingleViewWithChildren
还有个子类型RouteRecordRaw
,该类型定义了 children 类型必须为RouteRecordRaw[]
指定 routes 类型后,component数据类型也从 RawRouteComponent | null | undefined
。