Level 1:基础(Junior)
- 基本类型
- interface vs type
- any vs unknown
- enum
- readonly
- optional
- 泛型
↓
Level 2:React 开发(Mid)
- Component Props
- Generic Components
- forwardRef
- useRef
- Event 类型
- Custom Hook
- Context
- API 类型
↓
Level 3:Next.js 实战(Senior)
- Server Component 类型
- Server Action
- Route Handler
- Prisma Type
- React Query
- Zod
- Infer
- API Contract
↓
Level 4:TypeScript 高级(Senior+)
- extends
- keyof
- typeof
- indexed access
- mapped type
- conditional type
- infer
- utility types
- declaration merging
- module augmentation
↓
Level 5:架构级(Staff)
- 类型设计
- SDK Design
- API Design
- Library Design
- 类型性能
- 大型项目 Type Safety
type and interface?中文:
type 和 interface 有什么区别?什么时候应该使用它们?
Both type and interface can describe the shape of an object, and in many cases they are interchangeable.
The main difference is that interface is designed for object-oriented style and supports declaration merging, while type is more flexible because it can represent unions, intersections, tuples, primitive aliases, and function types.
In React projects, I usually use interface for component props or object structures, and type for utility types, unions, mapped types, and conditional types.
type 和 interface 都可以描述对象结构,大多数情况下可以互换。
主要区别是:
interface 支持声明合并(Declaration Merging)
interface 更适合描述对象
type 更灵活,可以表示:
实际项目中:
interfacetypeAlthough both work for object types, I prefer using interface for public object contracts and type for advanced type compositions because it keeps the code more consistent and readable.
any and unknown?中文:
any 和 unknown 有什么区别?
any completely disables type checking. Once a value is typed as any, TypeScript won't perform any compile-time validation, which can easily introduce runtime bugs.
unknown is a safer alternative. It accepts any value, but before using it, we must narrow its type with checks such as typeof, instanceof, or custom type guards.
In production projects, I avoid using any unless absolutely necessary and prefer unknown when dealing with external data such as API responses or user input.
any 会直接关闭 TypeScript 类型检查。
任何操作都会通过编译。
而 unknown 更安全。
虽然可以接受任何值,但是必须先进行类型缩小(Type Narrowing)才能使用。
例如:
生产环境中,应尽量避免 any,对于接口数据或用户输入,更推荐使用 unknown。
unknown forces developers to validate data before using it, which makes applications much safer when handling external or untrusted data.
中文:
什么是泛型?为什么要使用泛型?
Generics allow us to write reusable and type-safe code without knowing the exact type in advance.
Instead of using any, we define a type parameter that will be inferred when the function or component is used.
This improves both flexibility and type safety.
React heavily relies on generics, such as useState, useRef, and reusable components.
泛型可以让代码既保持通用性,又保证类型安全。
它不会像 any 一样丢失类型信息,而是在使用时由 TypeScript 自动推导具体类型。
React 中大量使用泛型,例如:
Generics allow us to write reusable components and utility functions while preserving precise type information, making large codebases much easier to maintain.
中文:
TypeScript 为什么要做类型推导?
Type inference allows TypeScript to automatically determine types without requiring explicit annotations everywhere.
It keeps the code cleaner while still providing autocomplete, compile-time checking, and refactoring support.
I usually rely on inference whenever the type is obvious, and only add explicit annotations when they improve readability or define public APIs.
类型推导可以减少大量重复的类型声明。
TypeScript 会根据变量初始值、函数返回值和上下文自动推断类型。
这样既能保持代码简洁,也不会失去类型检查能力。
一般来说,当类型很明显时,我会依赖类型推导;只有在公共接口或复杂逻辑中才显式声明类型。
interface extends and intersection (&)?中文:
extends 和 & 有什么区别?
Both can combine multiple types, but they behave differently.
extends creates a new interface by inheriting another interface and is mainly used in object-oriented designs.
The intersection operator (&) combines multiple types into one and works with any type, including objects, unions, and utility types.
In React projects, I usually use extends for component prop inheritance and & when composing multiple reusable type definitions.
二者都可以组合多个类型,但用途不同。
extends 表示继承,主要用于 interface。& 表示交叉类型,可以组合任意类型。实际项目中:
extends&extends expresses an inheritance relationship, while & simply merges multiple types together. Choosing the right one makes type definitions clearer and easier to maintain.
这一部分的问题都是 React / Next.js 面试中非常高频的 TypeScript 实战题,比基础语法更贴近真实项目。
中文:
React 组件的 Props 应该如何定义类型?
In most cases, I define component props using an interface because it clearly describes the public contract of the component.
If the props need to combine multiple reusable types, I may use a type with intersections.
For simple components, either approach works, but consistency across the project is more important than the choice itself.
大多数情况下,我会使用 interface 来定义组件的 Props,因为它能够清晰地描述组件的公共接口。
如果需要组合多个已有类型,我会使用 type 配合交叉类型(&)。
对于简单组件,两者都可以,但在团队中保持一致的风格更重要。
For public component APIs, I prefer using interface because it improves readability and supports declaration merging if needed. For more complex type composition, I use type.
React.FC no longer recommended?中文:
为什么现在很多项目都不推荐使用 React.FC?
React.FC was popular in earlier versions of React because it automatically included the children prop.
However, this behavior could make component APIs less explicit, since every component appeared to accept children even when it shouldn't.
Today, most teams prefer defining props directly and only adding children when the component actually needs it.
This makes component interfaces clearer and easier to maintain.
以前使用 React.FC 的主要原因是它会自动添加 children 属性。
但是这样会导致所有组件默认都能接收 children,即使实际上并不需要。
现在更推荐直接声明 Props,需要 children 时再显式添加。
这样组件接口更加明确,也更符合 React 当前的推荐实践。
I avoid React.FC because I prefer explicit component APIs. Only components that actually render children should declare a children prop.
useState?中文:
什么时候需要给 useState 指定泛型?
TypeScript can usually infer the state type from the initial value.
However, when the initial value is null, undefined, or an empty array or object, inference may not be sufficient.
In those cases, I explicitly provide the generic type to ensure correct type checking throughout the component.
如果初始值能够明确推导类型,例如字符串或数字,一般不需要手动指定泛型。
但是以下情况通常需要:
nullundefined这样可以避免错误的类型推导,提高类型安全。
I rely on type inference whenever possible, but I explicitly provide generic types when inference is ambiguous or when the state may contain multiple possible types.
useRef often null?中文:
为什么 useRef 通常初始化为 null?
When a component renders for the first time, the DOM element hasn't been created yet.
React assigns the actual DOM node to the ref after the component has been mounted.
Therefore, null accurately represents the initial state before the DOM becomes available.
组件第一次渲染时,DOM 还没有创建。
React 会在组件挂载完成后,把真实 DOM 赋值给 ref.current。
因此初始值使用 null 是最准确的表示。
The ref starts as null because the referenced DOM node doesn't exist during the initial render. React updates it during the commit phase after mounting.
中文:
React 中如何给事件定义类型?
React provides strongly typed event objects through its Synthetic Event system.
For example:
React.ChangeEvent<HTMLInputElement>React.ChangeEvent<HTMLTextAreaElement>React.MouseEvent<HTMLButtonElement>Using these built-in types provides autocomplete and compile-time safety.
React 已经提供了完整的事件类型。
常见的有:
React.ChangeEvent<HTMLInputElement>React.ChangeEvent<HTMLTextAreaElement>React.MouseEvent<HTMLButtonElement>直接使用这些官方类型即可获得完整的类型检查。
I always use React's built-in event types instead of any, which improves type safety and provides better editor support.
中文:
如何编写支持泛型的 React 组件?
Generic components allow us to build reusable UI while preserving the type of the data they receive.
A common example is a reusable table component.
Instead of accepting any[], the component accepts a generic type parameter, so TypeScript can infer the row type automatically.
This enables full autocomplete and compile-time validation for columns, render functions, and callbacks.
泛型组件可以在保持类型安全的同时,提高组件的复用性。
例如一个 DataTable<T> 组件。
组件并不知道具体的数据类型,而是在使用时由 TypeScript 自动推导。
这样列配置、回调函数和单元格渲染都能获得完整的类型提示。
Generic components make UI libraries much more scalable because they preserve the caller's data types without sacrificing type safety.
中文:
如何为自定义 Hook 定义类型?
A custom Hook should expose a well-defined API.
Its parameters, return values, and callbacks should all be explicitly typed.
If the Hook is reusable across different data models, I usually introduce generics to make it flexible while maintaining strong type safety.
自定义 Hook 应该像普通函数一样,明确声明:
如果 Hook 需要支持多种数据类型,可以引入泛型,提高复用性。
I treat custom Hooks as reusable APIs, so I always define clear input and output types to improve maintainability and developer experience.
forwardRef?中文:
forwardRef 应该如何定义类型?
When using forwardRef, both the ref type and the component props should be explicitly defined.
The first generic parameter specifies the ref type, while the second defines the props.
This ensures that parent components receive accurate autocomplete and compile-time validation when accessing the forwarded ref.
forwardRef 需要同时声明:
这样父组件在访问 ref.current 时,可以获得正确的类型提示和类型检查。
Explicitly typing both the ref and props makes forwarded components much easier to consume and prevents accidental runtime errors.
ComponentProps?中文:
什么是 ComponentProps?
ComponentProps extracts the props type from an existing React component.
Instead of redefining the same props manually, we can reuse the existing component's public interface.
This reduces duplication and keeps types synchronized as components evolve.
ComponentProps 可以从已有组件中提取 Props 类型。
这样无需重复定义相同的类型,也避免后续组件修改时出现类型不一致的问题。
它是构建可复用组件库时非常常用的工具类型。
I frequently use ComponentProps when wrapping third-party components, as it keeps my wrapper fully compatible with future API changes.
中文:
React 项目中最常见的 TypeScript 使用错误有哪些?
Some common mistakes include:
anynull and undefinedobjectas) to silence compiler errorsThese practices reduce type safety and make code harder to maintain.
常见问题包括:
anynull 和 undefinedobject)as 强制类型断言这些都会降低 TypeScript 的价值,并增加维护成本。
My goal is to let the type system prevent bugs instead of bypassing it. If I find myself using any or frequent type assertions, I usually rethink the type design first.
这一部分几乎是 Senior Frontend(React / Next.js) 面试的分水岭。
面试官通常不会问你定义,而是会问:
为什么要这样设计类型?它解决了什么问题?
因此,下面的答案都是偏项目实战的回答方式。
keyof and when do you use it?中文:
什么是 keyof?什么时候会使用它?
keyof creates a union of all property names from an object type.
Instead of manually listing property names, we can derive them directly from the type, which keeps the code synchronized as the object evolves.
In React and Next.js projects, I often use keyof for reusable table components, sorting fields, form validation, and generic utility functions.
keyof 可以获取一个对象类型的所有属性名,并组成联合类型。
它最大的作用是避免手动维护字段名称,当对象结构发生变化时,类型会自动同步更新。
在实际项目中,我经常把它用于:
keyof helps eliminate hard-coded property names and makes generic utilities much safer and easier to maintain.
typeof in TypeScript?中文:
TypeScript 中的 typeof 是什么?
Unlike JavaScript's runtime typeof, TypeScript's typeof extracts the type of an existing variable or function.
This allows us to reuse inferred types instead of redefining them manually.
It's especially useful when working with configuration objects, constants, or shared APIs.
TypeScript 的 typeof 用来获取变量或函数的类型。
它不是运行时操作,而是在编译阶段提取类型。
这样可以避免重复定义类型,提高代码一致性。
I often use typeof to derive types from configuration objects so that the implementation and the type definition always stay in sync.
中文:
什么是索引访问类型(Indexed Access Type)?
Indexed access types allow us to extract the type of a specific property from another type.
Instead of duplicating a property's type, we reference it directly from the source type.
This keeps related types consistent and reduces maintenance costs.
索引访问类型可以获取某个属性对应的类型。
例如,可以直接获取对象中某个字段的类型,而不需要重新定义。
这样可以保证多个类型始终保持一致。
Indexed access types reduce duplicated type definitions and are commonly used when building reusable business models.
中文:
什么是 Mapped Type?
Mapped types generate new object types by transforming each property of an existing type.
They are commonly used to make properties optional, readonly, or apply consistent transformations across an object.
Many built-in utility types, such as Partial, Required, and Readonly, are implemented using mapped types.
Mapped Type 可以遍历一个对象的所有属性,并生成新的类型。
它通常用于:
很多官方工具类型都是基于它实现的。
Mapped types allow us to transform entire object types consistently instead of manually rewriting every property.
中文:
什么是条件类型(Conditional Types)?
Conditional types allow TypeScript to choose different types based on a condition.
They enable more intelligent and reusable type definitions by expressing logic directly in the type system.
Many advanced utility types rely on conditional types internally.
条件类型可以根据条件返回不同的类型。
它让类型系统具有了一定的逻辑判断能力,因此能够构建更加灵活的泛型工具。
很多高级工具类型都是建立在条件类型之上的。
Conditional types make generic utilities much more expressive because they allow types to adapt automatically based on their inputs.
infer?中文:
什么是 infer?什么时候使用?
infer is used inside conditional types to capture and infer a type automatically.
Instead of manually specifying a type, TypeScript extracts it from another type.
This is commonly used to infer function return types, Promise values, or component props.
infer 用于条件类型内部,可以自动推导某个类型。
它最大的作用就是让 TypeScript 帮我们提取已有类型中的一部分,而不需要手动声明。
常见场景包括:
infer enables type extraction without duplication, making utility types much more reusable and maintainable.
中文:
TypeScript 常见的 Utility Types 有哪些?
Utility types are built-in generic helpers that simplify common type transformations.
Some of the most frequently used ones include:
PartialRequiredReadonlyPickOmitRecordExcludeExtractNonNullableReturnTypeUsing utility types helps reduce duplicated code and keeps type definitions concise.
Utility Types 是 TypeScript 提供的一组官方工具类型。
实际项目中最常用的有:
它们能够减少重复代码,提高类型复用性。
I prefer using utility types instead of redefining similar interfaces because they keep the type system consistent and much easier to evolve.
Pick and Omit?中文:
Pick 和 Omit 有什么区别?
Both are utility types used to derive new object types.
Pick selects only the specified properties.
Omit removes the specified properties and keeps the rest.
I frequently use them when creating API request and response models without duplicating type definitions.
两者都是从已有类型生成新类型。
区别在于:
Pick 保留指定字段。Omit 删除指定字段。在项目中,经常用于生成不同场景下的数据模型,例如创建请求参数或响应对象。
Using Pick and Omit keeps related models synchronized and avoids maintaining multiple nearly identical interfaces.
Record useful?中文:
为什么 Record 很有用?
Record creates an object type with a predefined set of keys and a consistent value type.
It's commonly used for dictionaries, configuration maps, localization resources, and lookup tables.
Compared with using a generic object type, Record provides much stronger compile-time guarantees.
Record 可以快速创建一个键和值都有明确类型的对象。
常用于:
相比普通对象,它具有更严格的类型检查。
Record makes configuration objects much safer because every required key must exist and every value follows the same type.
中文:
大型项目中,你如何设计可复用的 TypeScript 类型?
I avoid defining the same shape multiple times.
Instead, I create shared domain models and derive specialized types using utility types such as Pick, Omit, Partial, and mapped types.
For API contracts, I keep request and response types centralized so that both frontend and backend stay synchronized.
This approach reduces duplication, improves maintainability, and makes refactoring much safer.
在大型项目中,我尽量避免重复定义相同的数据结构。
通常会先定义领域模型(Domain Model),然后通过 Pick、Omit、Partial 等工具类型派生不同场景下的类型。
对于 API,我会集中维护请求和响应类型,确保前后端的数据契约保持一致。
这种设计方式可以减少重复代码,提高可维护性,也让后续重构更加安全。
My goal is to make types reusable rather than repeatedly writing similar interfaces. Well-designed types become part of the project's architecture, not just compiler annotations.
这一部分的问题几乎都是 React + Next.js 高级岗位 的真实面试题,重点考察你如何将 TypeScript 应用于 App Router、Server Components、Server Actions、Route Handlers 等实际开发场景,而不仅仅是掌握语法。
中文:
在 Next.js 项目中,你如何实现前后端共享 TypeScript 类型?
Since Next.js allows both frontend and backend code to live in the same project, I usually place shared domain models in a common directory, such as types or lib/types.
Both React components and Route Handlers can import these shared types directly, ensuring a single source of truth.
This approach keeps API contracts consistent, reduces duplication, and makes refactoring much safer.
由于 Next.js 的前后端代码通常在同一个项目中,我会把公共的数据模型放在 types 或 lib/types 等共享目录。
前端组件和 Route Handler 都直接引用这些类型,而不是各自重新定义。
这样可以:
I prefer defining shared domain models once and reusing them across the entire application instead of maintaining separate frontend and backend interfaces.
中文:
API 返回值应该如何定义类型?
I define explicit response models instead of relying on inferred or loosely typed objects.
Each API should have a clear response contract so that consumers know exactly what fields are available.
This improves autocomplete, compile-time validation, and reduces runtime errors.
我会为每个 API 定义明确的响应类型,而不是依赖 TypeScript 自动推导。
每个接口都应该有清晰的数据契约,让调用方能够准确知道返回的数据结构。
这样可以提高类型安全,也方便后续维护。
A well-defined response type becomes the API contract, making communication between frontend and backend much more reliable.
any for API data?中文:
为什么 API 返回值不应该使用 any?
Using any removes all compile-time validation, which means TypeScript can no longer detect incorrect property access or missing fields.
API data comes from external sources, so it should always be treated as potentially unsafe until properly validated.
Strong typing helps catch integration issues early during development.
API 数据来自外部系统,本身是不可信的。
如果使用 any,TypeScript 就无法检查字段是否存在或类型是否正确。
这会增加运行时错误的风险。
因此应该使用明确的数据类型,并在必要时结合数据校验工具进行验证。
The purpose of TypeScript is to prevent runtime bugs. Using any for external data defeats that purpose.
中文:
为什么很多 Next.js 项目都会使用 Zod?
TypeScript only performs compile-time type checking, while Zod validates data at runtime.
Together, they provide both static type safety and runtime validation.
This is especially important for API requests, form submissions, and user input, where external data cannot be fully trusted.
TypeScript 只能在编译阶段检查类型,运行时不会进行验证。
而 Zod 可以在运行时验证数据是否合法。
两者结合后,可以同时保证:
因此在表单、API、Server Actions 等场景中非常常见。
TypeScript verifies our code, while Zod verifies external data. They solve different problems and complement each other well.
z.infer?中文:
什么是 z.infer?
z.infer generates a TypeScript type directly from a Zod schema.
Instead of defining the schema and the interface separately, we define the schema once and derive the type automatically.
This eliminates duplication and guarantees that validation logic and TypeScript types stay synchronized.
z.infer 可以根据 Zod Schema 自动生成 TypeScript 类型。
这样无需再额外编写 interface 或 type。
Schema 和类型始终保持一致,避免维护两份定义。
I prefer schema-first development because validation rules and TypeScript types are always derived from the same source.
中文:
Server Action 应该如何定义类型?
Server Actions are simply asynchronous server-side functions.
Their parameters and return values should be explicitly typed, just like any other public API.
This provides better autocomplete and prevents accidental misuse by client components.
Server Action 本质上就是运行在服务器端的异步函数。
因此应该明确声明:
这样可以保证调用方获得完整的类型提示,也便于后续维护。
I treat Server Actions as part of the backend API layer, so I always define clear input and output types.
中文:
Route Handler 应该如何定义类型?
Each Route Handler should define the expected request data and the response model.
Instead of returning loosely typed objects, I ensure the response follows a consistent contract so that frontend consumers always know what to expect.
Route Handler 应该明确:
这样前端能够清楚知道接口返回的数据结构,也方便接口演进。
Consistent request and response models make APIs easier to maintain and significantly reduce frontend-backend integration issues.
中文:
React Query 应该如何结合 TypeScript?
I always define the data model returned by the query function.
Once the query function is properly typed, React Query can infer the types for data, loading states, and callbacks automatically.
This provides strong type safety without requiring repetitive annotations.
我会优先定义查询函数的返回类型。
React Query 会根据查询函数自动推导:
这样既减少重复声明,又能获得完整的类型提示。
Instead of manually typing every query result, I rely on inference from the query function to keep the code concise and maintainable.
中文:
如何保证 API Contract 的一致性?
I define shared request and response models that are reused across the application.
Whenever the API changes, both frontend and backend compile against the same types, allowing TypeScript to catch inconsistencies immediately.
This greatly reduces integration bugs.
我会维护统一的请求和响应类型。
前后端共同使用这些类型,而不是分别维护。
这样一旦接口发生变化,TypeScript 会立即提示所有受影响的位置,从而避免线上问题。
A shared API contract turns many runtime integration errors into compile-time errors, making deployments much safer.
中文:
大型 Next.js 项目中,你如何组织 TypeScript 类型?
I organize types based on business domains rather than creating one large global types file.
Each feature owns its local types, while shared domain models and utility types are placed in dedicated shared directories.
This structure keeps dependencies clear and prevents the type system from becoming difficult to navigate.
我更倾向于按照业务模块组织类型,而不是把所有类型都放到一个文件中。
通常会分为:
这样随着项目增长,类型系统仍然保持清晰和易维护。
I organize types around business domains because it scales much better than maintaining a single global types folder as the application grows.
很好,我们继续进入第五部分:TypeScript 架构设计(Staff / Senior+)。
这一部分和前面的最大区别是:
前面考的是:"你会不会 TypeScript?"
这一部分考的是:"你能不能设计一套类型系统?"
这些问题我见过在 TikTok、字节、Shopee、Grab、Airwallex、Stripe、Microsoft、Amazon 等公司的 Senior Frontend 面试中出现过。
中文:
如何设计一个可复用的 API Response 类型?
Instead of defining a different response type for every API, I prefer creating a generic response model.
Most APIs share common fields such as the response data, status code, message, or pagination metadata. A generic type allows us to reuse the same structure while keeping the payload type flexible.
This approach improves consistency across the project and reduces duplicated type definitions.
我不会为每个接口都单独定义一个 Response 类型,而是设计一个通用的响应模型。
大部分接口都有一些公共字段,例如:
通过泛型,可以让 data 保持灵活,同时复用统一的响应结构。
这样既减少重复代码,又能保证整个项目接口风格一致。
I try to model common API patterns once and reuse them through generics, rather than redefining nearly identical response types for every endpoint.
中文:
如何设计一个支持任何数据类型的 DataTable?
I make the component generic so it doesn't depend on a specific data model.
The table should infer the row type from the data it receives. Columns, cell renderers, sorting logic, and callbacks should all derive their types from that generic parameter.
This keeps the component reusable while providing full type safety and autocomplete.
我会把 DataTable 设计成泛型组件,而不是绑定某一种数据结构。
数据类型由调用方决定,列配置、单元格渲染、排序和事件回调都基于同一个泛型推导。
这样一个组件就可以服务多个业务模块,同时保持完整的类型安全。
A generic table should know nothing about the business model. It only understands the shape of the data through its type parameter.
中文:
什么样的 TypeScript API 才算设计得好?
A good TypeScript API should be easy to understand, easy to use, and difficult to misuse.
It should rely on type inference whenever possible, minimize the need for manual annotations, and expose clear error messages when developers use it incorrectly.
The best API feels natural because developers rarely need to think about its types.
好的 TypeScript API 应该具备几个特点:
同时应该充分利用类型推导,减少调用方手动写类型的负担。
如果使用方式错误,类型系统应该尽早给出明确的提示。
The best TypeScript APIs guide developers toward the correct usage instead of relying on documentation alone.
中文:
设计泛型时有哪些常见错误?
One common mistake is introducing generics when they aren't actually needed.
Another is using too many type parameters, which makes the API difficult to understand.
Overly complex conditional types can also slow down TypeScript compilation and make error messages much harder to interpret.
I prefer keeping generic APIs simple and introducing complexity only when it provides clear value.
常见错误包括:
我的原则是:先保持简单,再逐步扩展。
Generics should improve developer experience, not make the API harder to understand.
中文:
如何平衡类型安全和开发体验?
Type safety is important, but it shouldn't make the API difficult to use.
I rely on type inference as much as possible and only require explicit type annotations when they improve clarity.
The goal is to provide strong compile-time guarantees while keeping the developer experience smooth and intuitive.
类型安全固然重要,但不能为了类型而牺牲开发效率。
我会尽量利用 TypeScript 的自动推导能力,只有在必要时才要求开发者显式声明类型。
目标是在保证安全性的同时,让 API 使用起来足够自然。
Good type design should make the correct usage the easiest usage.
中文:
如何避免项目中出现大量重复的类型定义?
I start with shared domain models and derive specialized types using utility types such as Pick, Omit, and Partial.
Instead of creating separate interfaces for every use case, I build new types from existing ones.
This reduces duplication and keeps related models synchronized.
我会先定义领域模型(Domain Model)。
然后利用:
PickOmitPartial派生出不同业务场景需要的类型。
这样避免维护多份几乎相同的接口定义。
Whenever I notice similar interfaces appearing repeatedly, I consider extracting a shared model instead.
中文:
你的 TypeScript 使用理念是什么?
I see TypeScript as a design tool rather than just a type checker.
Good types document the system, prevent incorrect usage, and make large codebases easier to evolve.
My goal isn't to maximize the number of advanced types, but to create APIs that are safe, maintainable, and easy for other developers to understand.
我认为 TypeScript 不只是一个类型检查工具,更是一种设计工具。
好的类型应该能够:
我的目标不是追求复杂的高级类型,而是设计出简单、清晰、可靠的类型系统。
I use TypeScript to communicate intent. Well-designed types make the codebase easier to understand long before anyone reads the implementation.
这一部分是很多大厂 Senior Frontend 面试最后一轮会考察的内容。
重点不是让你背 Utility Types,而是看你是否理解:
MyPick<T, K>中文:
手写 TypeScript 内置工具类型 Pick<T, K>。
English:
Can you implement your own version of TypeScript's Pick utility type?
中文:
你能实现一个自己的 Pick 类型吗?
type MyPick<T, K extends keyof T> = { [P in K]: T[P]}Pick creates a new type by selecting specific properties from an existing type.
The important parts are:
K extends keyof T
[P in K]
T[P]
Pick 的作用是:
从已有对象类型中选择部分字段生成新的类型。
关键点:
xxxxxxxxxxK extends keyof T限制 K 必须是 T 存在的 key。
例如:
xinterface User { id: number; name: string; email: string;}type UserPreview = MyPick<User, "id" | "name">结果:
xxxxxxxxxx{ id: number; name: string;}
MyOmit<T, K>中文:
手写 Omit<T, K>。
xxxxxxxxxxtype MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>Omit removes specified properties from a type.
The implementation works by:
xxxxxxxxxxkeyof TxxxxxxxxxxExclude<keyof T, K>Omit 的逻辑:
例如:
xxxxxxxxxxinterface User { id:number; name:string; password:string;}type PublicUser =MyOmit<User,"password">得到:
xxxxxxxxxx{ id:number; name:string;}步骤:
第一步:
获取所有 key:
xxxxxxxxxx"id" | "name" | "password"第二步:
删除:
xxxxxxxxxx"password"第三步:
重新 Pick。
MyReadonly<T>中文:
实现 Readonly<T>。
xxxxxxxxxxtype MyReadonly<T> = { readonly [P in keyof T]: T[P]}Mapped types allow us to transform every property.
Here we iterate over all keys and add the readonly modifier.
Mapped Type 可以遍历对象所有属性。
这里:
xxxxxxxxxx[P in keyof T]遍历所有字段。
然后:
xxxxxxxxxxreadonly增加只读限制。
MyPartial<T>中文:
实现 Partial<T>。
xxxxxxxxxxtype MyPartial<T> = { [P in keyof T]?: T[P]}Partial converts all required properties into optional properties.
例如:
原始:
xxxxxxxxxxinterface User { id:number; name:string;}Partial:
xxxxxxxxxx{ id?:number; name?:string;}
MyReturnType<T>中文:
实现 ReturnType<T>。
xxxxxxxxxxtype MyReturnType<T> = T extends (args:any[]) => infer R ? R : never这是高级面试非常喜欢问的一题。
核心:
xxxxxxxxxxinferThe conditional type checks whether T is a function.
If it is, infer R captures the return type.
Otherwise, it returns never.
这里:
xxxxxxxxxxT extends (args:any[])=>infer R表示:
如果 T 是函数类型:
提取它的返回值类型。
例如:
xxxxxxxxxxfunction getUser(){ return { id:1, name:"Tom" }}type Result =MyReturnType<typeof getUser>得到:
xxxxxxxxxx{ id:number; name:string;}
MyParameters<T>中文:
实现 Parameters<T>。
xxxxxxxxxxtype MyParameters<T> = T extends (args:infer P)=>any ? P : neverInstead of extracting the return type, we extract the function arguments.
例如:
xxxxxxxxxxfunction login( username:string, password:string){}结果:
xxxxxxxxxx[ string, string]
DeepPartial<T>中文:
实现深度 Partial。
Make every nested property optional.
xxxxxxxxxxtype DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]}原始:
xxxxxxxxxxinterface User { profile:{ name:string; age:number; }}DeepPartial:
xxxxxxxxxx{ profile?:{ name?:string; age?:number; }}面试官关注:
你是否理解:
Flatten<T>中文:
实现数组扁平化类型。
xxxxxxxxxxtype Flatten<T> = T extends Array<infer U> ? U : Txxxxxxxxxxtype A = Flatten<string[]>结果:
xxxxxxxxxxstring
中文:
如何设计一个类型安全的 EventEmitter?
Instead of allowing arbitrary strings as event names, I define an event map where each event has its own payload type.
The emitter methods use generic constraints to ensure that the event name and payload always match.
传统 EventEmitter:
xxxxxxxxxxemit( event:string, data:any)问题:
任何事件都可以传任何数据。
更好的设计:
定义 Event Map:
xxxxxxxxxxinterface Events { login:{ userId:string } logout:{ timestamp:number }}然后:
xxxxxxxxxxemit<K extends keyof Events>( event:K, payload:Events[K])这样:
xxxxxxxxxxemit( "login", { userId:"123" })正确。
但是:
xxxxxxxxxxemit( "login", { timestamp:123 })会报错。
中文:
如何设计一个类型安全的 DataTable?
xxxxxxxxxxinterface Column<T>{ key:keyof T; title:string; render?( value:T[keyof T], row:T ):React.ReactNode;}interface DataTableProps<T>{ data:T[]; columns:Column<T>[];}xxxxxxxxxxinterface User{ id:number; name:string; email:string;}<DataTable<User>data={users}columns={[ { key:"name", title:"Name" }]}/>核心思想:
DataTable 不知道业务数据是什么。
它只知道:
"我接受一个 T 类型的数据,然后所有配置都基于 T 推导。"
这就是大型组件库的设计方式。
这一部分是实际工作中最常遇到的问题。
面试官通常不会再问:
"什么是 interface?"
而会问:
"如果你负责一个大型 Next.js 项目,你如何设计 TypeScript 架构?"
useFetch<T>() hook?中文:
如何设计一个类型安全的 useFetch<T>() Hook?
English:
How would you create a reusable fetch hook that provides type safety for different API responses?
如何设计一个通用请求 Hook,同时保证不同 API 返回不同类型的数据?
I would make the hook generic so that the caller can specify or infer the response type.
The hook should not know business details. It only handles request states such as:
The API layer defines the actual response models.
我会使用泛型设计:
xxxxxxxxxxuseFetch<T>()让调用方决定返回的数据类型。
Hook 本身只负责:
业务数据类型由 API 层提供。
xxxxxxxxxxinterface ApiResponse<T>{ data:T; message:string;}function useFetch<T>( url:string){ const [data,setData] = useState<T | null>(null); const [loading,setLoading] = useState(false); return { data, loading };}xxxxxxxxxxinterface User{ id:number; name:string;}const { data}=useFetch<User[]>("/api/users");TypeScript 自动知道:
xxxxxxxxxxdata是:
xxxxxxxxxxUser[] | nullA generic hook keeps the fetching logic reusable while allowing each feature module to maintain its own domain types.
中文:
React Query 如何进行类型设计?
I avoid manually typing the returned data everywhere.
Instead, I make sure the query function has a correct return type.
React Query can infer the type automatically from the query function.
我不会在每个组件里面重复声明:
xxxxxxxxxxconst data:User[]而是保证:
queryFn 本身类型正确。
React Query 会自动推导。
API:
xxxxxxxxxxasync function getUsers(){ const res = await fetch("/api/users"); return res.json() as Promise<User[]>;}Query:
xxxxxxxxxxconst { data}=useQuery({ queryKey:["users"], queryFn:getUsers});自动推导:
xxxxxxxxxxdata:User[] | undefinedI prefer inference from the source function because it keeps types centralized and avoids duplicated annotations.
中文:
Next.js + Prisma 项目中,数据库类型应该如何设计?
I avoid exposing Prisma models directly to the frontend.
Database models represent storage, while API models represent business contracts.
I usually create DTO types and transform Prisma results before returning them.
不要直接把 Prisma Model 暴露给前端。
原因:
数据库结构 ≠ 前端业务模型。
例如:
数据库:
xxxxxxxxxxUser { passwordHash:string;}但是:
前端:
xxxxxxxxxxUserResponse { id:number; name:string;}所以应该:
Database Model
↓
DTO
↓
Frontend Type
Prisma:
xxxxxxxxxxconst user =await prisma.user.findMany();转换:
xxxxxxxxxxconst result =users.map(user=>({ id:user.id, name:user.name}));返回:
xxxxxxxxxxUserDTO[]Separating database models and API DTOs gives us flexibility to change storage without breaking frontend contracts.
中文:
Next.js Server Action 如何设计类型?
I treat Server Actions like backend functions.
They should have:
For complex data, I usually combine TypeScript with Zod.
我把 Server Action 看作后端 API。
应该包含:
复杂场景:
TypeScript + Zod。
Schema:
xxxxxxxxxxconst schema =z.object({ email:z.string().email(), password:z.string()});Type:
xxxxxxxxxxtype LoginInput =z.infer<typeof schema>;Action:
xxxxxxxxxxasync function login( input:LoginInput){}TypeScript provides developer safety, while Zod protects the application at runtime.
中文:
如何设计前后端共享 DTO?
I create a dedicated domain layer containing shared contracts.
For example:
xxxxxxxxxxsrc├── app├── components├── server└── contracts├── user.ts├── product.ts└── order.ts
The frontend and backend both depend on these contracts.
我会建立独立的 Contract Layer:
例如:
xxxxxxxxxxcontractsuser.tsproduct.tsorder.ts
里面只保存:
The contract layer becomes the communication boundary between frontend and backend.
中文:
如何设计类型安全的 React Form?
I usually combine:
The schema defines validation rules and automatically generates the TypeScript type.
This prevents the form type and validation rules from becoming inconsistent.
常见方案:
xxxxxxxxxxZod Schema↓z.infer↓React Hook Form
Example:
xxxxxxxxxxconst schema =z.object({ username:z.string(), age:z.number()});type FormData =z.infer<typeof schema>;Form:
xxxxxxxxxxuseForm<FormData>()这样:
全部类型安全。
Schema-first form design reduces duplication because validation rules and TypeScript types share the same source.
中文:
大型项目如何避免 TypeScript 类型爆炸?
Type complexity can become a real problem in large applications.
I avoid:
Instead, I prefer simple domain models and clear boundaries between modules.
大型项目中 TypeScript 也可能成为性能问题。
避免:
例如:
xxxxxxxxxxA extends B ?C extends D ?嵌套太深。
不要:
xxxxxxxxxxComponent< T, K, V, R, X>除非真的需要。
不要:
一个 type 文件包含:
Type systems should model business complexity, not create additional complexity.
中文:
如何优化 TypeScript 编译性能?
For large projects, TypeScript performance can become an issue.
Common optimizations include:
skipLibCheck大型项目中 TypeScript 编译速度可能下降。
常见优化:
拆分 tsconfig:
xxxxxxxxxxfrontendbackendshared
例如:
xxxxxxxxxx{ "skipLibCheck":true}减少第三方库检查。
减少 TypeScript 需要分析的范围。