DrillLab
第 09 / 09 节LESSON 09 / 09约 12 分钟~12 min

泛型参数,以及怎么读 tsc 的报错Generic parameters, and how to read a tsc error

useState<Note[]> 那对尖括号在说什么,和 react-notes-app 那 10 个构建错误的真相。What the angle brackets in useState<Note[]> say, and the real cause of the 10 build errors in react-notes-app.

2 个练习2 exercises地基 · 第 3 部分Foundations · Part 3
这一页有什么On this page6
学完这节你会After this lesson you can
  • 看懂 useState<Note[]>([]) 和 Task<T> 里的尖括号Read the angle brackets in useState<Note[]>([]) and in Task<T>
  • 会读 tsc 报错的四个部分:文件、位置、错误码、说明Read the four parts of a tsc error: file, position, error code, explanation
  • 能分辨「我的代码错了」和「项目配置本身有问题」Tell the difference between a mistake in your code and a problem in the project setup
  • 知道常见错误码 TS2304 / TS2582 / TS2345 各是什么意思Know what the common codes TS2304, TS2582 and TS2345 each mean
这在考试里考什么What the exam does with this

react-notes-app 的 npm run build 在原始状态下就是失败的 —— 10 个 tsc 错误,全部来自测试文件的类型配置缺失。能不能认出「这不是我的问题」,直接决定你会不会浪费半小时。In react-notes-app, npm run build fails as delivered. All 10 tsc errors come from missing type settings for the test files. Recognising that the fault is not yours is what decides whether you lose half an hour.

这节课要看的真实文件Real files this lesson looks at2 项 · 2 个可以展开看原文2 items · 2 can be opened
react-notes-app/tsconfig.jsoninclude 了 src,但没配 vitest 全局类型It includes src but does not configure the vitest global types
JSONtsconfig.json源项目From source
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "lib": ["ES2020", "DOM", "DOM.Iterable"],
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "jsx": "react-jsx",
8 "strict": true,
9 "skipLibCheck": true,
10 "noEmit": true,
11 "esModuleInterop": true
12 },
13 "include": ["src", "q2"]
14}
Source: react-notes-app/tsconfig.json
react-notes-app/src/NoteManager.test.tsx报错就出在这个文件This is the file the error comes from
TSXNoteManager.test.tsx源项目From source
1import { render, screen } from "@testing-library/react";
2import userEvent from "@testing-library/user-event";
3import NoteManager from "./components/NoteManager";
4
5test("adds a note", async () => {
6 render(<NoteManager />);
7 await userEvent.type(screen.getByTestId("form-input"), "My Title");
8 await userEvent.type(screen.getByTestId("form-textarea"), "My Content");
9 await userEvent.click(screen.getByTestId("form-submit-button"));
10
11 expect(screen.getByTestId("notes-list")).toHaveTextContent("My Title");
12});
13
14test("submit button disabled when inputs empty", () => {
15 render(<NoteManager />);
16 expect(screen.getByTestId("form-submit-button")).toBeDisabled();
17});
18
19test("deletes a note", async () => {
20 render(<NoteManager />);
21 await userEvent.type(screen.getByTestId("form-input"), "ToDelete");
22 await userEvent.type(screen.getByTestId("form-textarea"), "x");
23 await userEvent.click(screen.getByTestId("form-submit-button"));
24 await userEvent.click(screen.getByRole("button", { name: "Delete" }));
25
26 expect(screen.getByTestId("notes-list")).not.toHaveTextContent("ToDelete");
27});
28
29test("edits a note in place", async () => {
30 render(<NoteManager />);
31 await userEvent.type(screen.getByTestId("form-input"), "Old");
32 await userEvent.type(screen.getByTestId("form-textarea"), "c1");
33 await userEvent.click(screen.getByTestId("form-submit-button"));
34
35 await userEvent.click(screen.getByRole("button", { name: "Edit" }));
36 expect(screen.getByTestId("form-submit-button")).toHaveTextContent("Update");
37
38 const input = screen.getByTestId("form-input");
39 await userEvent.clear(input);
40 await userEvent.type(input, "New");
41 await userEvent.click(screen.getByTestId("form-submit-button"));
42
43 expect(screen.getByTestId("notes-list")).toHaveTextContent("New");
44 expect(screen.getByTestId("notes-list")).not.toHaveTextContent("Old");
45});
Source: react-notes-app/src/NoteManager.test.tsx
§01

尖括号:告诉泛型「这次装的是什么」Angle brackets: telling a generic what it holds this time

泛型(generic)就是一个「留了洞的类型」,调用的人负责填。A generic is a type with a hole left in it. Whoever calls it fills the hole.

useState 是 React 提供的函数,它不可能知道你要存什么 —— 可能是数字、可能是字符串、可能是笔记数组。 所以它的类型定义留了一个洞,写成 useState<S>。 你调用时用尖括号把洞填上:

为什么第一个必须显式写 <Note[]>? 因为初始值是 [] —— 一个空数组, TypeScript 从它身上只能推断出「某种数组」,不知道装什么。 不写的话后面 setNotes([...prev, note]) 就会报类型错。

而第三个 useState("") 不用写, 因为初始值 "" 已经把类型说清楚了:string规则:推断得出来就别写,推断不出来才写。

useState is a function React hands you, and it has no way of knowing what you plan to store — a number, a string, an array of notes. So its type definition leaves a hole in it, written useState<S>. You fill the hole with angle brackets when you call it:

Why does the first one have to spell out <Note[]>? Because the initial value is [] — an empty array, and all TypeScript can infer from it is “some kind of array”, with no idea what goes in. Leave it off and setNotes([...prev, note]) later throws a type error.

The third one, useState(""), needs nothing, because the initial value "" has already settled the type: string. The rule: if it can be inferred, do not write it; write it only when it cannot.

TSX三处真实的 useStateThree real uses of useState源项目From source
1const [notes, setNotes] = useState<Note[]>([]); // 必须写:[] 看不出装什么
2const [noteToEdit, setNoteToEdit] = useState<Note | null>(null); // 必须写:null 看不出
3const [title, setTitle] = useState(""); // 不用写:"" 就是 string
1const [notes, setNotes] = useState<Note[]>([]); // required: [] does not say what it holds
2const [noteToEdit, setNoteToEdit] = useState<Note | null>(null); // required: null says nothing
3const [title, setTitle] = useState(""); // not needed: "" means string
Source: react-notes-app/src/components/NoteManager/index.tsx 与 NoteForm/index.tsx
TypeScriptq2/taskRunner.ts:自己定义泛型q2/taskRunner.ts: defining your own generic源项目From source
1export type Task<T> = () => Promise<T>;
2
3export async function runTasks<T>(
4 tasks: Task<T>[],
5 limit: number,
6): Promise<SettledResult<T>[]> { ... }
Source: react-notes-app/q2/taskRunner.ts
这里的 T 是「任务成功时返回什么类型」。runTasks 自己不关心 T 到底是什么,它只负责保证:你给我 Task<string>[],我还你 SettledResult<string>[]。这就是泛型的价值 —— 同一份实现服务所有类型。Here T is the type a task returns when it succeeds. runTasks does not care what T actually is. It only guarantees one thing: hand it Task<string>[] and it hands back SettledResult<string>[]. That is what a generic buys you — one implementation that serves every type.
§02

tsc 报错的四个部分The four parts of a tsc error

拿一条真实的报错拆开看:

  1. 文件 src/NoteManager.test.tsx —— 哪个文件
  2. 位置 (5,1) —— 第 5 行第 1 列
  3. 错误码 TS2582 —— 可以直接搜的编号
  4. 说明 —— 人话描述,而且这条还带了修复建议

永远从第一条错误看起。TypeScript 的报错常常会连锁: 一个类型错了,后面十处用到它的地方全跟着报。修掉第一条, 后面九条可能自己就没了。

Take one real error apart:

  1. File src/NoteManager.test.tsx — which file
  2. Position (5,1) — line 5, column 1
  3. Error code TS2582 — a number you can search directly
  4. Message — a plain-language description, and this one even suggests a fix

Always start from the first error. TypeScript errors chain constantly: one type goes wrong, and the ten places that use it all report too. Fix the first one and the other nine may disappear by themselves.

Terminal本机实测输出Output measured on this machine已跑通Verified
1$ npx tsc --noEmit
2
3src/NoteManager.test.tsx(5,1): error TS2582: Cannot find name 'test'. Do you need to
4 install type definitions for a test runner? Try `npm i --save-dev @types/jest` or
5 `npm i --save-dev @types/mocha`.
6src/NoteManager.test.tsx(11,3): error TS2304: Cannot find name 'expect'.
7src/NoteManager.test.tsx(14,1): error TS2582: Cannot find name 'test'.
8...共 10 条,全部在这一个文件里
1$ npx tsc --noEmit
2
3src/NoteManager.test.tsx(5,1): error TS2582: Cannot find name 'test'. Do you need to
4 install type definitions for a test runner? Try `npm i --save-dev @types/jest` or
5 `npm i --save-dev @types/mocha`.
6src/NoteManager.test.tsx(11,3): error TS2304: Cannot find name 'expect'.
7src/NoteManager.test.tsx(14,1): error TS2582: Cannot find name 'test'.
8...10 in total, all in this one file
§03

实测:这 10 个错误不是你写的代码的问题Tried for real: these 10 errors are not caused by the code you wrote

这是 react-notes-app 自带的配置缺陷。认出它,别去改业务代码。It is a setup defect that ships with react-notes-app. Recognise it, and leave your own code alone.

看清三件事,就能确定「不是我的问题」:

  1. 报错全在测试文件里,一条都不在src/components/ 下。
  2. 报的是 testexpect 找不到 —— 这两个不是你写的,是测试框架注入的全局变量。
  3. npx vitest run 实测 4 个测试全过。也就是说代码逻辑完全正确,只是 tsc 不认识这两个全局名字。

根因:tsconfig.jsoninclude: ["src", "q2"]把测试文件也纳入了类型检查,但没有任何地方告诉 tsc 「这些全局变量存在」。缺的是"types": ["vitest/globals"](或者在测试文件里显式 import)。

考场上该怎么办?npm run build 失败但 npx vitest run 全过时, 先确认失败发生在 tsc 那一步、而且只涉及测试文件的全局名字。 确认之后:继续用 vitest 验证你的实现,并在提交说明里点出这个配置问题。把它当成一个观察记下来,而不是当成一个要你修的任务 —— 题目没有要求你改配置,而擅自改 tsconfig 有可能影响判卷。

Three things, once you see them clearly, settle that this is not your problem:

  1. Every error is in the test file, not one of them under src/components/.
  2. What it cannot find is test and expect — you did not write those two; the test framework injects them as globals.
  3. npx vitest run passes all 4 tests, measured. Which means the code logic is entirely correct and tsc simply does not know those two global names.

Root cause: the include: ["src", "q2"] in tsconfig.json pulls the test file into type checking, but nothing anywhere tells tsc that those globals exist. What is missing is "types": ["vitest/globals"] (or an explicit import inside the test file).

What should you do in the exam? When npm run build fails but npx vitest run passes, first confirm the failure happens at the tsc step and only involves global names from the test file. Once confirmed: keep using vitest to verify your implementation, and point out the config problem in your submission notes. Record it as an observation, not as a task you were asked to fix — nothing in the question asks you to change config, and editing tsconfig on your own initiative could affect grading.

JSONtsconfig.json(原样)tsconfig.json (unchanged)源项目From source
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "lib": ["ES2020", "DOM", "DOM.Iterable"],
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "jsx": "react-jsx",
8 "strict": true,
9 "skipLibCheck": true,
10 "noEmit": true,
11 "esModuleInterop": true
12 },
13 "include": ["src", "q2"]
14}
Source: react-notes-app/tsconfig.json
注意最后一行:include 里有 src,而测试文件就在 src 下。compilerOptions 里没有 types 字段,所以 vitest 的全局变量对 tsc 是不存在的。Look at the last line: include holds src, and the test file sits under src. compilerOptions has no types field, so as far as tsc is concerned the vitest globals do not exist.
§04

几个会真的遇到的错误码The error codes you will actually meet

错误码意思通常的原因
TS2304Cannot find name 'X'名字打错、没 import、或者缺全局类型声明
TS2582Cannot find name 'test'2304 的特化版,专门提示你缺测试框架类型
TS2345参数类型不匹配传了 string 给要 number 的参数(比如 id 类型搞混)
TS2339属性不存在拼错字段名,或者对象类型不是你以为的那个
TS2531 / TS18047可能是 nullstrictNullChecks:用之前没判断
TS7006参数隐式为 anynoImplicitAny:回调参数没写类型
CodeMeaningUsual cause
TS2304Cannot find name 'X'Typo in the name, no import, or a missing global type declaration
TS2582Cannot find name 'test'The specialised form of 2304, telling you the test framework types are missing
TS2345Argument type mismatchPassed a string where a number was wanted (mixing up an id type, say)
TS2339Property does not existMisspelled field name, or the object is not the type you assumed
TS2531 / TS18047Possibly nullstrictNullChecks: you did not check before using it
TS7006Parameter implicitly has an any typenoImplicitAny: the callback parameter has no type written
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L1认出来Spot it这是谁的问题Whose problem is this

你刚 clone 好 react-notes-app,一行代码都还没写,先跑了npm run build,得到 10 个TS2582: Cannot find name 'test'。 同时 npx vitest run 显示 4 个测试全过。 最合理的判断是?

You have just cloned react-notes-app, written not one line of code, and run npm run build. You get 10 of TS2582: Cannot find name 'test'. At the same time npx vitest run shows all 4 tests passing. What is the most sensible conclusion?

先选一个选项Pick an option first
L2填空Fill the blanks补全泛型参数Fill in the generic parameters

两处真实的泛型用法。想清楚「TypeScript 能不能自己推断出来」。

Two real uses of generics. For each one, work out whether TypeScript can infer it on its own.

TSX两个真实片段Two real snippets3 个空3 blanks
1// NoteManager:两个 state
2const [notes, setNotes] = useState<>([]);
3const [noteToEdit, setNoteToEdit] = useState<Note | >(null);
4
5// q2/taskRunner.ts:自定义泛型
6export type Task<T> = () => <T>;
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

useState 初始值是 [] 或 nullThe starting value of useState is [] or null
显式写泛型参数Write the generic parameter yourself
一堆 tsc 报错A long list of tsc errors
只看第一条,后面可能是连锁Read only the first one. The rest may follow from it
报错全在测试文件、说全局名字找不到Every error is in a test file and says a global name cannot be found
缺测试框架类型,不是你的逻辑问题The types of the test framework are missing. Your logic is not the problem
TS2345 参数类型不匹配TS2345, an argument type does not match
回去看类型定义,通常是 id 的 number/string 搞混Go back to the type definition. Usually an id is a number where a string was expected, or the other way round
这节的要点What to take away
  1. 泛型是「留洞的类型」,尖括号是你在填洞。A generic is a type with a hole in it. The angle brackets are you filling the hole.
  2. 初始值看不出类型(空数组、null)时必须显式写泛型参数。When the starting value shows no type, as with an empty array or null, write the generic parameter yourself.
  3. tsc 报错四件套:文件、行列、错误码、说明。永远先看第一条。A tsc error has four parts: file, line and column, error code, explanation. Always read the first error first.
  4. react-notes-app 的 npm run build 原生失败,10 个错全在测试文件,与你的实现无关。npm run build fails in react-notes-app as delivered. All 10 errors are in test files and have nothing to do with your work.
  5. 分辨「我的错」和「项目的错」:看报错位置、报的是谁的名字、测试跑不跑得过。To tell your own mistake from a project defect, look at where the error points, whose name it complains about, and whether the tests still pass.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 这一门读完了 —— 去验收Course finished — go get checked考场:空文件夹、计时、没有提示按钮The arena: an empty folder, a clock, no hint button
    去考场To the arena
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 类型、type 与 interfaceTypes, type and interface