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

类型、type 与 interfaceTypes, type and interface

Note 和 NoteFormProps 这两个真实类型,把该讲的都讲全了。Two real types from the project, Note and NoteFormProps, cover everything you need here.

1 个练习1 exercises地基 · 第 3 部分Foundations · Part 3
这一页有什么On this page5
学完这节你会After this lesson you can
  • 会给变量、函数参数、返回值标类型Give a type to a variable, to a function parameter and to a return value
  • 分清 type 和 interface 各自的场合(以及为什么这题里两个都用了)Know when type fits and when interface fits, and why this project uses both
  • 会写可选字段、联合类型、函数类型Write an optional field, a union type and a function type
  • 知道 strict: true 意味着什么Know what strict: true means
这在考试里考什么What the exam does with this

react-notes-app 是 strict 模式的 TypeScript 项目。props 类型写错、少写一个字段,构建就过不去。而两个考试的核心数据结构(Note、Order)都是从类型定义读起的。react-notes-app is a TypeScript project in strict mode. Get a props type wrong, or leave out one field, and the build fails. And in both exams the main data shapes, Note and Order, are read from their type definitions first.

这节课要看的真实文件Real files this lesson looks at3 项 · 3 个可以展开看原文3 items · 3 can be opened
react-notes-app/src/types/Note.ts整个 Q1 的数据形状The shape of all the Q1 data
TypeScriptNote.ts源项目From source
1export type Note = {
2 id: number;
3 title: string;
4 content: string;
5};
Source: react-notes-app/src/types/Note.ts
react-notes-app/src/components/NoteForm/index.tsxprops 类型的真实写法How the prop types are actually written
TSXindex.tsx源项目From source
1import React, { useState, useEffect } from "react";
2import type { Note } from "../../types/Note";
3
4interface NoteFormProps {
5 onSubmit: (note: Note) => void;
6 noteToEdit: Note | null;
7}
8
9const NoteForm: React.FC<NoteFormProps> = ({ onSubmit, noteToEdit }) => {
10 const [title, setTitle] = useState("");
11 const [content, setContent] = useState("");
12
13 useEffect(() => {
14 if (noteToEdit) {
15 setTitle(noteToEdit.title);
16 setContent(noteToEdit.content);
17 } else {
18 setTitle("");
19 setContent("");
20 }
21 }, [noteToEdit]);
22
23 const isFormInvalid = title.trim() === "" || content.trim() === "";
24
25 const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
26 event.preventDefault();
27 if (isFormInvalid) return;
28 const newNote = {
29 id: noteToEdit ? noteToEdit.id : Date.now(),
30 title: title.trim(),
31 content: content.trim(),
32 };
33 onSubmit(newNote);
34 setTitle("");
35 setContent("");
36 };
37
38 return (
39 <div className="card w-200 pt-30 pb-8 mt-15 mb-15">
40 <form onSubmit={handleSubmit} data-testid="note-form">
41 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
42 <label className="form-title-label">Title:</label>
43 <input
44 type="text"
45 placeholder="Title"
46 value={title}
47 onChange={(e) => setTitle(e.target.value)}
48 data-testid="form-input"
49 className="form-input"
50 />
51 </section>
52
53 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
54 <label className="form-content-label">Content:</label>
55 <textarea
56 placeholder="Content"
57 value={content}
58 onChange={(e) => setContent(e.target.value)}
59 data-testid="form-textarea"
60 className="form-textarea"
61 />
62 </section>
63
64 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
65 <button
66 type="submit"
67 disabled={isFormInvalid}
68 data-testid="form-submit-button"
69 >
70 {noteToEdit ? "Update" : "Add"}
71 </button>
72 </section>
73 </form>
74 </div>
75 );
76};
77
78export default NoteForm;
Source: react-notes-app/src/components/NoteForm/index.tsx
react-notes-app/tsconfig.jsonstrict: true
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
§01

从这个项目最重要的 3 行代码开始Start with the 3 most important lines in this project

整个 Q1 的数据结构就这么多。That is the whole data shape of Q1.

读一个陌生项目,先找类型定义。 它比任何 README 都准确 —— README 会过时,类型不会 (改了类型不匹配的代码,编译器立刻报错)。

这 3 行告诉了你几件关键的事:

  • idnumber,不是 string。 所以后面比较要写 note.id !== id, 而项目里生成 id 用的是 Date.now()(返回数字)。
  • 三个字段都是必填(没有 ?)。 所以你构造一个新 Note 时,三个都得给,少一个编译不过。
  • 没有 createdAt、没有 done —— 别自己加字段。

Reading an unfamiliar project, find the type definitions first. They beat any README for accuracy — a README goes stale, types do not (change code so it no longer matches a type and the compiler complains on the spot).

These 3 lines tell you several important things:

  • id is a number, not a string. So comparisons later on read note.id !== id, and the project generates ids with Date.now() (which returns a number).
  • All three fields are required (no ?). So when you build a new Note you have to supply all three; miss one and it will not compile.
  • No createdAt, no done — do not invent fields.
TypeScriptsrc/types/Note.ts源项目From source
1export type Note = {
2 id: number;
3 title: string;
4 content: string;
5};
Source: react-notes-app/src/types/Note.ts
§02

type 和 interface:这个项目里两个都用了type and interface: this project uses both

Note 用的是 type, 而 NoteFormProps 用的是 interface。 这不是随便选的,但也不是必须这么选 —— 大多数情况下两者可以互换。

typeinterface
描述对象形状
联合类型 A | B✓ 只能用它
同名重复声明会自动合并✗ 报错✓ 会合并
习惯用法数据形状、联合、别名props、可能被扩展的公开契约

实用建议:照项目里已有的风格写。考试不会因为你用 type 还是 interface 扣分,但风格不一致会让人皱眉。 Q2 那边就必须用 type —— 因为SettledResult 是个联合类型,interface 做不到。

Note uses type, while NoteFormProps uses interface. That was not random, but it was not required either — most of the time the two are interchangeable.

typeinterface
Describe an object shape
Union type A | B✓ the only option
Two declarations of one name merge automatically✗ error✓ they merge
Conventional useData shapes, unions, aliasesProps, public contracts that may get extended

Practical advice: write whatever style the project already uses. No exam docks points for type versus interface, but inconsistency makes people wince. Q2 has no choice but type — because SettledResult is a union, and interface cannot do that.

TSXsrc/components/NoteForm/index.tsx源项目From source
1interface NoteFormProps {
2 onSubmit: (note: Note) => void; // 函数类型:收一个 Note,不返回东西
3 noteToEdit: Note | null; // 联合类型:要么是 Note,要么是 null
4}
1interface NoteFormProps {
2 onSubmit: (note: Note) => void; // function type: takes one Note, returns nothing
3 noteToEdit: Note | null; // union type: either a Note or null
4}
Source: react-notes-app/src/components/NoteForm/index.tsx
两个字段各演示了一种写法。onSubmit 的 (note: Note) => void 是「函数类型」;noteToEdit 的 Note | null 是「联合类型」—— 用 null 表示「现在不在编辑任何东西」。Each field shows one form. The (note: Note) => void on onSubmit is a function type; the Note | null on noteToEdit is a union type, where null means nothing is being edited right now.
TypeScriptq2/taskRunner.ts源项目From source
1export type SettledResult<T> =
2 | { status: "fulfilled"; value: T }
3 | { status: "rejected"; reason: unknown };
Source: react-notes-app/q2/taskRunner.ts
这叫「可辨识联合」:两个分支都有 status 字段,而且值是不同的字面量。于是你写 if (r.status === "fulfilled") 之后,TypeScript 就知道这个分支里一定有 value 而没有 reason。This is a discriminated union: both branches carry a status field, and each one holds a different literal value. So once you write if (r.status === "fulfilled"), TypeScript knows that inside that branch there is a value and no reason.
§03

strict: true 意味着什么What strict: true means

react-notes-app/tsconfig.json 里写了"strict": true。它是一个开关包,一次打开好几项检查。 对你影响最大的两项:

  • strictNullChecks ——nullundefined 不再能随便赋给别的类型。 所以 noteToEdit 必须明确写成 Note | null, 而且用它之前必须先判断。这就是为什么真实代码里到处是if (noteToEdit)
  • noImplicitAny —— 推断不出类型的参数不许留空。所以事件处理器要写(e: React.FormEvent<HTMLFormElement>), 不能光写 (e)

main.tsx 里那个 ! 就是 strictNullChecks 的产物:document.getElementById("root") 的类型是HTMLElement | null,而 createRoot不接受 null。! 是在说「我保证它不是 null」。

react-notes-app/tsconfig.json sets "strict": true. It is a bundle of switches that turns on several checks at once. The two that hit you hardest:

  • strictNullChecks null and undefined can no longer be handed to other types freely. So noteToEdit has to be spelled out as Note | null, and you have to check it before using it. That is why the real code is full of if (noteToEdit).
  • noImplicitAny — a parameter whose type cannot be inferred may not be left bare. So the event handler is written (e: React.FormEvent<HTMLFormElement>), not just (e).

That ! in main.tsx is a product of strictNullChecks: document.getElementById("root") has type HTMLElement | null, and createRoot does not accept null. The ! is you saying “I guarantee this is not null”.

TSX源项目From source
1ReactDOM.createRoot(document.getElementById("root")!).render(...)
2// ↑
3// 非空断言:告诉编译器「相信我,这里不会是 null」
1ReactDOM.createRoot(document.getElementById("root")!).render(...)
2// ↑
3// non-null assertion: tells the compiler this will not be null
Source: react-notes-app/src/main.tsx
练习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.

L2填空Fill the blanks补全 NoteTable 的 props 类型Fill in the props type of NoteTable

这是 NoteTable 真实的 props 类型。 三个空:一个数组类型、一个函数类型的参数、一个函数类型的返回值。

This is the real props type of NoteTable. Three blanks: an array type, a parameter of a function type, and the return value of a function type.

TSXsrc/components/NoteTable/index.tsx3 个空3 blanks
1export interface NoteTableProps {
2 notes: ;
3 onDelete: (id: ) => void;
4 onEdit: (note: Note) => ;
5}
把 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.

读一个陌生项目Reading a project you have never seen
先找 types/ 或 *.d.ts,类型比 README 准Look for types/ or *.d.ts first. The types are more accurate than the README
「要么是 X 要么没有」Either an X, or nothing
X | null,用之前先 if 判断X | null, with an if check before you use it
需要联合类型You need a union type
只能用 type,interface 做不到Only type can do this. interface cannot
Object is possibly 'null'Object is possibly 'null'
先判断,或者确实安全时用 !Check it first, or use ! when you are sure it is safe
这节的要点What to take away
  1. 读项目先读类型定义:Note 的 3 行决定了 Q1 全部的数据操作。Read the type definitions first. The 3 lines of Note decide every data operation in Q1.
  2. type 和 interface 大多可互换;联合类型只能用 type。type and interface are interchangeable most of the time. Only type can express a union.
  3. (note: Note) => void 是函数类型;Note | null 是联合类型。(note: Note) => void is a function type. Note | null is a union type.
  4. strict: true 打开后,null 必须显式处理、参数必须有类型。With strict: true, null has to be handled explicitly and every parameter needs a type.
  5. ! 是非空断言,是你在替编译器担保,用错了运行时才炸。! is a non-null assertion. With it you tell the compiler that the value is not null, so it stops checking. If you are wrong, the failure appears only when the code runs.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises1 个,就在这一页上面 —— 别攒着最后一起做1 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson泛型参数,以及怎么读 tsc 的报错Generic parameters, and how to read a tsc error
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: ESM:import / export 与那些莫名其妙的报错ESM: import / export, and the errors that look strange at first