TypeScript 5.8+ Stage 3 Decorator: Metaprogramming, DI & Xác thực Runtime trong môi trường sản xuất

Mục lục bài viết(8 mục)
TypeScript 5.8+ hỗ trợ nguyên bản Decorator ECMAScript Stage 3, đánh dấu một sự thay đổi then chốt so với cờ experimentalDecorators cũ. Việc triển khai nguyên bản này cung cấp một cơ chế tiêu chuẩn hóa, mạnh mẽ cho metaprogramming, cho phép các mẫu mạnh mẽ như Dependency Injection (DI) và xác thực runtime mà không cần dựa vào các tính năng không chuẩn hoặc polyfill. Hướng dẫn này trình bày chi tiết ứng dụng thực tế của các decorator này, tập trung vào kiến trúc, triển khai và tiện ích sản xuất của chúng.
Tìm hiểu Decorator Stage 3
Decorator Stage 3 hoạt động trên các phần tử lớp trong quá trình định nghĩa, không phải khởi tạo. Chúng là các hàm nhận các ngữ cảnh cụ thể và cho phép sửa đổi hoặc thay thế phần tử được trang trí. Không giống như experimentalDecorators, đặc tả mới cung cấp một API dễ đoán và mạnh mẽ hơn, bao gồm quyền truy cập vào các trường riêng tư và khả năng thêm các trình khởi tạo.
Các loại và chữ ký Decorator
Chữ ký hàm decorator thay đổi tùy theo mục tiêu:
-
Class Decorators: Áp dụng cho các khai báo lớp.
typescripttype ClassDecorator = <T extends Function>( value: T, context: ClassDecoratorContext ) => T | void;contextcung cấpkind: 'class',name, vàaddInitializer. -
Method Decorators: Áp dụng cho các phương thức trong một lớp.
typescripttype MethodDecorator = <T extends Function>( value: T, context: ClassMethodDecoratorContext ) => T | void;contextcung cấpkind: 'method',name,static,private,access, vàaddInitializer. -
Getter/Setter Decorators: Áp dụng cho các bộ truy cập.
typescripttype AccessorDecorator = <T>( value: ClassAccessorDecoratorTarget<T>, context: ClassAccessorDecoratorContext<ThisType<T>, T> ) => ClassAccessorDecoratorResult<ThisType<T>, T> | void;contextcung cấpkind: 'getter'hoặc'setter',name,static,private,access, vàaddInitializer. -
Field Decorators: Áp dụng cho các trường lớp.
typescripttype FieldDecorator = <T>( value: undefined, // Field decorators receive undefined as value context: ClassFieldDecoratorContext<ThisType<T>, T> ) => ((initialValue: T) => T) | void;contextcung cấpkind: 'field',name,static,private,access, vàaddInitializer. Giá trị trả về là một hàm sửa đổi giá trị ban đầu của trường. -
Auto-Accessor Decorators: Áp dụng cho các thuộc tính
accessor.typescripttype AutoAccessorDecorator = <T>( value: ClassAccessorDecoratorTarget<T>, context: ClassAccessorDecoratorContext<ThisType<T>, T> ) => ClassAccessorDecoratorResult<ThisType<T>, T> | void;Tương tự như getter/setter, nhưng dành cho cú pháp
accessorkết hợp.
Cấu hình
Đảm bảo tsconfig.json được cấu hình cho các decorator nguyên bản:
{
"compilerOptions": {
"target": "ES2022", // Or higher
"module": "ESNext",
"lib": ["ES2022", "DOM"],
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "Bundler", // Or Node16/NodeNext
"experimentalDecorators": false, // Crucial: Disable legacy decorators
"emitDecoratorMetadata": false, // Crucial: Disable legacy metadata
"useDefineForClassFields": true // Recommended for class fields
}
}
Trường hợp sử dụng 1: Lightweight Dependency Injection Container
Một DI container không phụ thuộc nào chứng minh các decorator lớp và trường để quản lý vòng đời dịch vụ và inject các dependency.
// di.ts
// A simple map to store registered services and their instances
const serviceRegistry = new Map<string | symbol, { constructor: new (...args: any[]) => any; instance?: any; singleton: boolean }>();
// Symbol for internal metadata storage on classes
const INJECTABLE_METADATA = Symbol('injectable_metadata');
/**
* Class decorator to mark a class as injectable and register it with the DI container.
* @param singleton If true, only one instance of the class will be created and reused.
*/
export function Injectable(singleton: boolean = true) {
return <T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>
) => {
if (context.kind !== 'class') {
throw new Error(`@Injectable can only be applied to classes, not ${String(context.kind)}`);
}
const serviceName = target.name;
if (serviceRegistry.has(serviceName)) {
console.warn(`Service "${serviceName}" already registered. Overwriting.`);
}
serviceRegistry.set(serviceName, { constructor: target, singleton });
// Store metadata about dependencies for this class
context.addInitializer(function(this: T) {
// This runs after the class is defined, but before any instances are created.
// We can use 'this' to refer to the class constructor itself.
// Any @Inject() decorators would have added metadata to this.
});
return target; // Return the original class
};
}
/**
* Property decorator to inject a dependency into a class field.
* The field type annotation is used to determine the dependency to inject.
*/
export function Inject(targetService?: new (...args: any[]) => any) {
return <T, V>(
value: undefined, // Field decorators receive undefined as value
context: ClassFieldDecoratorContext<T, V>
) => {
if (context.kind !== 'field') {
throw new Error(`@Inject can only be applied to fields, not ${String(context.kind)}`);
}
// This initializer runs when an instance of the class is created.
context.addInitializer(function(this: T) {
// 'this' refers to the instance of the class being constructed.
// We need to determine the type of the field to inject.
// In TypeScript, runtime type information for fields is not directly available
// without emitDecoratorMetadata (which we're avoiding) or explicit type passing.
// For simplicity, we'll rely on `targetService` if provided, or assume the field name
// corresponds to a registered service name. A more robust solution might use a symbol
// or string literal for the service key.
const fieldName = String(context.name);
let serviceKey: string | symbol;
if (targetService) {
serviceKey = targetService.name;
} else {
// Fallback: assume field name is the service name.
// This is less robust and prone to naming conflicts.
// A better approach would be to require @Inject(ServiceClass)
serviceKey = fieldName.charAt(0).toUpperCase() + fieldName.slice(1); // e.g., 'logger' -> 'Logger'
console.warn(`@Inject on field '${fieldName}' without explicit service. Assuming service name '${String(serviceKey)}'. Consider @Inject(ServiceClass).`);
}
const serviceEntry = serviceRegistry.get(serviceKey);
if (!serviceEntry) {
throw new Error(`Service "${String(serviceKey)}" not found in DI container for injection into field "${fieldName}".`);
}
if (serviceEntry.singleton && serviceEntry.instance) {
// Reuse existing singleton instance
(this as any)[fieldName] = serviceEntry.instance;
} else {
// Create new instance, resolving its dependencies recursively
const instance = resolve(serviceEntry.constructor);
if (serviceEntry.singleton) {
serviceEntry.instance = instance; // Store for future reuse
}
(this as any)[fieldName] = instance;
}
});
// Field decorators return a function to modify the initial value.
// Since we're injecting, we don't need an initial value from the decorator.
return;
};
}
/**
* Resolves an instance of a service from the DI container.
* Handles recursive dependency resolution.
*/
export function resolve<T>(constructor: new (...args: any[]) => T): T {
const serviceName = constructor.name;
const serviceEntry = serviceRegistry.get(serviceName);
if (!serviceEntry) {
throw new Error(`Service "${serviceName}" not registered with DI container.`);
}
if (serviceEntry.singleton && serviceEntry.instance) {
return serviceEntry.instance as T;
}
// Recursively resolve constructor arguments
// This is a simplified approach. A real DI container would inspect constructor
// parameters for @Inject annotations or type metadata.
// For this example, we assume constructor args are also Injectable services
// or primitive types not managed by DI.
const dependencies: any[] = []; // In a real system, this would be populated by inspecting constructor params
const instance = new constructor(...dependencies);
// The @Inject field decorators will run their initializers *after* the constructor completes
// and before the instance is fully returned from `new constructor()`.
// This is handled by the JS engine's class construction process.
if (serviceEntry.singleton) {
serviceEntry.instance = instance;
}
return instance;
}
// --- Usage Example ---
interface ILogger {
log(message: string): void;
}
@Injectable(true)
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[ConsoleLogger] ${message}`);
}
}
@Injectable(false) // Not a singleton
class TransientService {
private id: number;
constructor() {
this.id = Math.random();
console.log(`TransientService instance created: ${this.id}`);
}
getId(): number {
return this.id;
}
}
@Injectable(true)
class UserService {
@Inject(ConsoleLogger) // Explicitly inject ConsoleLogger
private logger!: ILogger; // Use definite assignment assertion
@Inject() // Implicitly inject TransientService (by field name convention)
private transientService!: TransientService;
getUser(id: string): string {
this.logger.log(`Fetching user ${id}`);
return `User ${id} (Transient ID: ${this.transientService.getId()})`;
}
}
// Resolve the root service
const userService = resolve(UserService);
console.log(userService.getUser('123'));
const anotherUserService = resolve(UserService);
console.log(anotherUserService.getUser('456'));
// Verify singleton behavior for UserService and ConsoleLogger
console.log('userService === anotherUserService:', userService === anotherUserService); // Should be true
// Verify transient behavior for TransientService
// The transientService injected into userService and anotherUserService should be different instances
// This is because @Inject() creates a new instance for each injection point if the service is not a singleton.
// However, since UserService itself is a singleton, the same TransientService instance will be injected
// into the *same* UserService instance. To see different TransientService instances, we'd need to
// resolve UserService multiple times if it were *not* a singleton, or inject TransientService into
// different non-singleton services.
// Let's demonstrate by resolving TransientService directly:
const ts1 = resolve(TransientService);
const ts2 = resolve(TransientService);
console.log('ts1 === ts2:', ts1 === ts2); // Should be false, as TransientService is not a singleton
// Expected output:
// TransientService instance created: 0.xxxx
// TransientService instance created: 0.yyyy
// [ConsoleLogger] Fetching user 123
// User 123 (Transient ID: 0.xxxx)
// [ConsoleLogger] Fetching user 456
// User 456 (Transient ID: 0.xxxx)
// userService === anotherUserService: true
// TransientService instance created: 0.zzzz
// TransientService instance created: 0.aaaa
// ts1 === ts2: false
DI container này sử dụng @Injectable để đăng ký các lớp và @Inject để đánh dấu các trường cho dependency injection. Callback addInitializer trong field decorator rất quan trọng: nó chạy sau constructor của lớp nhưng trước khi instance được trả về hoàn toàn, cho phép trường được điền với dependency đã được giải quyết.
Trường hợp sử dụng 2: High-Throughput Runtime Schema Validation
Xác thực runtime rất quan trọng đối với các ranh giới API và tính toàn vẹn dữ liệu. Decorator có thể tự động hóa việc áp dụng và xác thực schema. Chúng ta sẽ sử dụng một thư viện xác thực đơn giản hóa.
// validator.ts
type ValidatorFn = (value: any) => string | null; // Returns error message or null if valid
const VALIDATION_METADATA = Symbol('validation_metadata');
interface FieldValidation {
propertyName: string | symbol;
validators: ValidatorFn[];
}
/**
* Stores validation rules on the class prototype.
*/
function addValidationRule(target: Object, propertyName: string | symbol, validator: ValidatorFn) {
if (!Reflect.hasOwnMetadata(VALIDATION_METADATA, target)) {
Reflect.defineMetadata(VALIDATION_METADATA, [], target);
}
const validations = Reflect.getOwnMetadata(VALIDATION_METADATA, target) as FieldValidation[];
let fieldValidation = validations.find(fv => fv.propertyName === propertyName);
if (!fieldValidation) {
fieldValidation = { propertyName, validators: [] };
validations.push(fieldValidation);
}
fieldValidation.validators.push(validator);
}
/**
* Decorator factory for 'required' validation.
*/
export function Required() {
return <T, V>(
value: undefined,
context: ClassFieldDecoratorContext<T, V>
) => {
if (context.kind !== 'field') {
throw new Error(`@Required can only be applied to fields, not ${String(context.kind)}`);
}
context.addInitializer(function(this: T) {
addValidationRule(Object.getPrototypeOf(this), context.name, (val) =>
val === null || val === undefined || (typeof val === 'string' && val.trim() === '')
? `${String(context.name)} is required.`
: null
);
});
};
}
/**
* Decorator factory for 'minLength' validation.
*/
export function MinLength(length: number) {
return <T, V extends string>(
value: undefined,
context: ClassFieldDecoratorContext<T, V>
) => {
if (context.kind !== 'field') {
throw new Error(`@MinLength can only be applied to fields, not ${String(context.kind)}`);
}
context.addInitializer(function(this: T) {
addValidationRule(Object.getPrototypeOf(this), context.name, (val) =>
typeof val === 'string' && val.length < length
? `${String(context.name)} must be at least ${length} characters long.`
: null
);
});
};
}
/**
* Decorator factory for 'max' validation.
*/
export function Max(maxValue: number) {
return <T, V extends number>(
value: undefined,
context: ClassFieldDecoratorContext<T, V>
) => {
if (context.kind !== 'field') {
throw new Error(`@Max can only be applied to fields, not ${String(context.kind)}`);
}
context.addInitializer(function(this: T) {
addValidationRule(Object.getPrototypeOf(this), context.name, (val) =>
typeof val === 'number' && val > maxValue
? `${String(context.name)} must be at most ${maxValue}.`
: null
);
});
};
}
/**
* Class decorator to enable validation for a class.
* It adds a `validate()` method to the class prototype.
*/
export function Validatable() {
return <T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>
) => {
if (context.kind !== 'class') {
throw new Error(`@Validatable can only be applied to classes, not ${String(context.kind)}`);
}
// Add a validate method to the class prototype
context.addInitializer(function(this: T) {
Object.defineProperty(this.prototype, 'validate', {
value: function(this: any): string[] {
const errors: string[] = [];
const validations = Reflect.getOwnMetadata(VALIDATION_METADATA, Object.getPrototypeOf(this)) as FieldValidation[] || [];
for (const fieldValidation of validations) {
const value = this[fieldValidation.propertyName];
for (const validator of fieldValidation.validators) {
const error = validator(value);
if (error) {
errors.push(error);
}
}
}
return errors;
},
writable: true,
configurable: true,
});
});
return target;
};
}
// Polyfill for Reflect.metadata if not available (e.g., in some environments or older TS versions)
// For native decorators, Reflect.metadata is not strictly required by the spec itself,
// but it's a common pattern for storing metadata.
// If you're using a bundler like Webpack/Rollup/ESBuild, ensure 'reflect-metadata' is imported once at the entry point.
// `import 'reflect-metadata';`
// For this example, we'll assume it's available or provide a minimal shim.
if (typeof Reflect === 'undefined' || !Reflect.hasOwnMetadata) {
console.warn("Reflect.metadata not found. Providing a minimal shim. For production, consider 'reflect-metadata' polyfill.");
const metadataMap = new WeakMap<object, Map<string | symbol, any>>();
(Reflect as any).defineMetadata = (key: string | symbol, value: any, target: object, propertyKey?: string | symbol) => {
let targetMetadata = metadataMap.get(target);
if (!targetMetadata) {
targetMetadata = new Map();
metadataMap.set(target, targetMetadata);
}
const metadataKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : String(key);
targetMetadata.set(metadataKey, value);
};
(Reflect as any).getOwnMetadata = (key: string | symbol, target: object, propertyKey?: string | symbol) => {
const targetMetadata = metadataMap.get(target);
if (!targetMetadata) return undefined;
const metadataKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : String(key);
return targetMetadata.get(metadataKey);
};
(Reflect as any).hasOwnMetadata = (key: string | symbol, target: object, propertyKey?: string | symbol) => {
const targetMetadata = metadataMap.get(target);
if (!targetMetadata) return false;
const metadataKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : String(key);
return targetMetadata.has(metadataKey);
};
}
// --- Usage Example ---
@Validatable()
class Product {
@Required()
@MinLength(3)
name: string;
@Required()
@Max(9999)
price: number;
description?: string;
constructor(name: string, price: number, description?: string) {
this.name = name;
this.price = price;
this.description = description;
}
}
// Test cases
const product1 = new Product('Laptop', 1200);
const errors1 = (product1 as any).validate();
console.log('Product 1 errors:', errors1); // Expected: []
const product2 = new Product('', 50000);
const errors2 = (product2 as any).validate();
console.log('Product 2 errors:', errors2); // Expected: ["name is required.", "name must be at least 3 characters long.", "price must be at most 9999."]
const product3 = new Product('TV', 10000);
const errors3 = (product3 as any).validate();
console.log('Product 3 errors:', errors3); // Expected: ["price must be at most 9999."]
const product4 = new Product('A', 100);
const errors4 = (product4 as any).validate();
console.log('Product 4 errors:', errors4); // Expected: ["name must be at least 3 characters long."]
// Demonstrate that validate method is added to prototype
console.log('Product.prototype has validate method:', 'validate' in Product.prototype); // true
Hệ thống xác thực này sử dụng các field decorator (@Required, @MinLength, @Max) để gắn các quy tắc xác thực vào các thuộc tính. Class decorator @Validatable sau đó inject một phương thức validate vào prototype của lớp. Phương thức này lặp qua các quy tắc đã thu thập và thực thi chúng đối với các thuộc tính của instance. addInitializer trong các field decorator đảm bảo rằng các quy tắc xác thực được đăng ký trên prototype của lớp trước khi bất kỳ instance nào được tạo, làm cho phương thức validate có sẵn ngay lập tức. Lưu ý việc sử dụng Reflect.defineMetadata và Reflect.getOwnMetadata để lưu trữ các quy tắc xác thực, điều này yêu cầu polyfill reflect-metadata (hoặc một shim như đã cung cấp).
So sánh kiến trúc: Stage 3 Decorator vs. Legacy experimentalDecorators
| Tính năng | Stage 3 Decorator (TS 5.8+) | Legacy experimentalDecorators (TS < 5.8) |
|---|---|---|
| Trạng thái đặc tả | ECMAScript Stage 3 (gần hoàn thiện) | Không chuẩn, thử nghiệm |
tsconfig.json | experimentalDecorators: false, emitDecoratorMetadata: false | experimentalDecorators: true, emitDecoratorMetadata: true (cho DI) |
| Ngữ cảnh Decorator | ClassDecoratorContext phong phú, ClassMethodDecoratorContext, v.v. | Không có đối tượng ngữ cảnh rõ ràng; các đối số là target, key, descriptor |
addInitializer | Có, cho phép chạy code sau định nghĩa lớp hoặc sau khi xây dựng instance | Không có tương đương trực tiếp; thường yêu cầu thiết lập thuộc tính tĩnh thủ công |
| Field Decorator | Nhận undefined dưới dạng value, trả về hàm khởi tạo | Nhận target, key; sửa đổi descriptor (nếu là accessor) hoặc target |
| Trường riêng tư | Có thể trang trí và tương tác với các trường riêng tư thông qua access | Không thể trang trí các trường riêng tư |
Reflect.metadata | Không yêu cầu bởi đặc tả; được sử dụng cho siêu dữ liệu tùy chỉnh | Phụ thuộc nhiều vào phản ánh kiểu (ví dụ: cho DI) |
| Hành vi Runtime | Decorator chạy trong quá trình định nghĩa lớp | Decorator chạy trong quá trình định nghĩa lớp |
| Hiệu suất | Thường được tối ưu hóa, một phần của runtime công cụ JS | Có thể gây ra chi phí do Reflect.metadata và polyfill |
| Khả năng bảo trì | API tiêu chuẩn hóa, ổn định lâu dài hơn | API có thể thay đổi, ít dự đoán hơn |
Những vấn đề và cách khắc phục trong sản xuất
-
experimentalDecoratorsvẫn được bật:- Triệu chứng: Decorator hoạt động không mong muốn, hoặc TypeScript phàn nàn về cú pháp decorator ngay cả với TS 5.8+.
- Nguyên nhân: Bạn có thể vẫn còn
"experimentalDecorators": truetrongtsconfig.jsoncủa mình. - Cách khắc phục: Đặt
"experimentalDecorators": falsevà"emitDecoratorMetadata": false. Đảm bảotargetcủa bạn làES2022trở lên vàmoduleResolutionlàBundlerhoặcNodeNext.
-
Thiếu
Reflect.metadata:- Triệu chứng: Lỗi runtime như
Reflect.defineMetadata is not a functionkhi sử dụng các mẫu metadata (như trong ví dụ xác thực). - Nguyên nhân: Polyfill
reflect-metadatakhông được import hoặc không có sẵn trong môi trường runtime của bạn. Các decorator Stage 3 nguyên bản không cung cấpReflect.metadata. - Cách khắc phục: Thêm
import 'reflect-metadata';ở đầu điểm vào ứng dụng của bạn. Đảm bảo góireflect-metadatađã được cài đặt (npm install reflect-metadata).
- Triệu chứng: Lỗi runtime như
-
Thông tin kiểu tại Runtime:
- Triệu chứng: Trong DI, bạn muốn inject
ServiceAvàoclass MyClass { @Inject() serviceA: ServiceA; }nhưng decorator không thể xác định kiểu củaServiceA. - Nguyên nhân: Các kiểu TypeScript bị xóa trong quá trình biên dịch.
emitDecoratorMetadata(cung cấp thông tin kiểu thông quaReflect.metadata) dành cho các decorator cũ và không tương thích với Stage 3. - Cách khắc phục: Truyền rõ ràng kiểu cho decorator, ví dụ:
@Inject(ServiceA). Đối với constructor injection, bạn cần phân tíchFunction.prototype.toString()của constructor hoặc sử dụng bước xây dựng để trích xuất các kiểu.
- Triệu chứng: Trong DI, bạn muốn inject
-
Thứ tự thực thi của Decorator:
- Triệu chứng: Hành vi không mong muốn khi nhiều decorator được áp dụng cho cùng một mục tiêu.
- Nguyên nhân: Decorator được áp dụng theo một thứ tự cụ thể:
- Field/Method/Accessor: Từ trên xuống dưới, sau đó được đánh giá từ dưới lên trên.
- Class: Được áp dụng sau khi tất cả các thành viên của nó được trang trí.
- Các callback
addInitializerchạy theo thứ tự chúng được thêm vào.
- Cách khắc phục: Hiểu thứ tự thực thi. Nếu decorator A phụ thuộc vào đầu ra của decorator B, hãy đảm bảo B chạy trước. Đối với
addInitializer, chúng chạy theo thứ tự chúng được đăng ký.
-
Ngữ cảnh
thistrongaddInitializer:- Triệu chứng:
thisbên trongaddInitializertham chiếu đến đối tượng sai. - Nguyên nhân: Đối với
ClassDecoratorContext,thistham chiếu đến constructor của lớp. Đối vớiClassFieldDecoratorContext,ClassMethodDecoratorContext, v.v.,thistham chiếu đến instance đang được xây dựng. - Cách khắc phục: Lưu ý ngữ cảnh
this. Đối với các hoạt động cấp lớp (như thêm một phương thức prototype), sử dụngObject.getPrototypeOf(this)hoặcthis.prototypenếuthislà constructor. Đối với các hoạt động cấp instance (như đặt giá trị trường),thistrực tiếp tham chiếu đến instance.
- Triệu chứng:
Câu hỏi thường gặp
-
Tôi có thể sử dụng Stage 3 Decorator với React/Angular/Vue không? Có. Các framework hiện đại đang thích nghi. Angular 17+ hỗ trợ đầy đủ các decorator Stage 3. React và Vue không sử dụng decorator cho các thành phần cốt lõi của chúng nhưng có thể hưởng lợi từ chúng trong các lớp tiện ích hoặc dịch vụ. Đảm bảo công cụ xây dựng của bạn (Webpack, Vite, v.v.) được cấu hình để xử lý chúng (ví dụ: Babel với
@babel/plugin-proposal-decoratorsở chế độ2023-11). -
Stage 3 Decorator có chậm hơn các hàm thông thường không? Decorator thực thi tại thời điểm định nghĩa lớp, không phải tại runtime cho mỗi lần tạo instance (ngoại trừ các callback
addInitializerchạy trong quá trình xây dựng instance). Chi phí là tối thiểu và thường không đáng kể đối với hầu hết các ứng dụng. Tác động hiệu suất chính đến từ những gì các decorator làm (ví dụ: phản ánh phức tạp hoặc tính toán nặng). -
Làm cách nào để debug decorator? Đặt các câu lệnh
debugger;bên trong các hàm decorator của bạn. Vì chúng chạy trong quá trình định nghĩa lớp, trình gỡ lỗi của bạn sẽ tạm dừng khi lớp đang được định nghĩa. Đối với các callbackaddInitializer, chúng sẽ tạm dừng trong quá trình tạo instance. -
Tôi có thể trang trí các tham số constructor không? Đề xuất Stage 3 Decorator hiện tại không bao gồm các decorator tham số. Đây là một tính năng của
experimentalDecoratorsnhưng đã bị loại bỏ do sự phức tạp và lo ngại về phản ánh kiểu runtime. Đối với DI với các tham số constructor, bạn thường cần cung cấp rõ ràng các dependency hoặc sử dụng bước xây dựng để trích xuất thông tin kiểu. -
Sự khác biệt giữa
valuevàcontexttrong các đối số decorator là gì?valuelà thứ đang được trang trí (ví dụ: hàm phương thức, constructor của lớp). Đối với các field decorator,valuelàundefinedvì các trường không có "giá trị" ban đầu theo cách mà các phương thức có.contextcung cấp siêu dữ liệu về phần tử được trang trí, chẳng hạn nhưkind(lớp, phương thức, trường),name, trạng tháistatic, trạng tháiprivatevà hàmaddInitializerquan trọng.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

TypeScript thôi là chưa đủ: Đảm bảo an toàn kiểu dữ liệu end-to-end với Zod trong Next.js
TypeScript biến mất khi runtime. Tìm hiểu lý do tại sao các kiểu tĩnh thất bại ở ranh giới API và Server Action của bạn, và cách Zod mang lại xác thực schema không thể sai sót và suy luận kiểu.
Read more
React 19 Actions trong thực tế: useActionState, useOptimistic & khả năng phục hồi của Server Action
Hướng dẫn toàn diện về React 19 Actions trong thực tế: useActionState, useOptimistic và khả năng phục hồi của Server Action với kiến trúc cấp độ sản phẩm và ví dụ mã.
Read more
Kiểm thử Vue Components trong Trình duyệt Thực: Kiến trúc, Tính phản ứng và QUnit
Hướng dẫn thực tế về kiểm thử các Vue 3 component trong trình duyệt thực bằng QUnit: đồng bộ hóa DOM nextTick, đột biến form và cô lập fixture.
Read more