使用 Object.defineProperty 后如何读取打字稿中的属性?
How to read properties in typescript after using Object.defineProperty?
我有 following code on typescript playground 并且出现了一些我不确定如何开始工作的问题
class PathInfo {
functionName: string;
httpPath: string;
httpMethod: string;
constructor(functionName: string, httpPath: string, httpMethod: string) {
this.functionName = functionName;
this.httpPath = httpPath;
this.httpMethod = httpMethod;
}
toString(): string {
return "PathInfo["+this.functionName+","+this.httpPath+","+this.httpMethod+"]";
}
}
class AuthRequest {}
class AuthResponse {}
class LoginRequest {}
class LoginResponse {}
const path: any = (thePath: string, type: any) => {
return (target: Function, memberName: string, propertyDescriptor: PropertyDescriptor) => {
const pathMeta = new PathInfo(memberName, path, type);
Object.defineProperty(target, memberName+'pathInfo', {
value: pathMeta,
writable: false
});
//How do I access the stored pathMeta
//console.log("target="+target.pathInfo);
console.log("member="+memberName);
console.log("props="+propertyDescriptor);
}
}
class AuthApiImpl {
@path("/authenticate", AuthResponse)
authenticate(request: AuthRequest): Promise<AuthResponse> {
throw new Error("all this is generated by factory.createApiImpl");
}
@path("/login", LoginResponse)
login(request: LoginRequest): Promise<LoginResponse> {
throw new Error("all this is generated by factory.createApiImpl");
}
};
function printMethods(obj: any) {
console.log("starting to print methods");
for (var id in obj) {
console.log("id="+id);
try {
//How do I access the stored pathMeta here FOR EACH METHOD ->
//console.log("target="+target.pathInfo);
if (typeof(obj[id]) == "function") {
console.log(id+":"+obj[id].toString());
}
} catch (err) {
console.log(id + ": inaccessible"+err);
}
}
}
console.log("starting to run")
const temp = new AuthApiImpl();
printMethods(temp);
console.log("done")
- 第64-65行,如何读取我设置的属性
- 40-41行,如何读取我设置的属性
- 第 58-74 行,为什么没有打印任何函数?我想打印所有函数,我不想打印属性(只是函数)
- 第 33 行,此时我可以访问 class 名称吗?
- 第 35 行,我认为 target 是一个函数并且会被授权,然后登录,但是如果我将 属性 定义为 JUST 'pathInfo',我得到一个错误 属性 已经在目标上定义(这意味着目标是 class 而不是函数?)。我很困惑。
非常抱歉,因为我试图专注于一个问题,但是当我深入打字稿世界时,这个编写装饰器的测试给了我更多的问题而不是答案。
如何调整代码以在此处播放更多内容?
这里的目标是当开发人员定义其他微服务的 API 时,我可以捕获一堆元信息并将其存储在某个地方,我可以稍后在启动代码中使用。我不在乎我真正存储它的位置,但只需要一种清晰的方式来了解我想要扩展的 class、方法、return 类型、http 路径等
如何获取 class
的方法
去掉装饰器还是抢不到方法名。这不是特定于 TypeScript 的问题。
您需要获取原型的属性,而不仅仅是对象本身。
function printMethods(obj: any) {
console.log("starting to print methods");
const objProto = Object.getPrototypeOf(obj);
console.log(Object.getOwnPropertyNames(objProto));
}
如何访问 class 个名字
目前不认为装饰器可以做到这一点,但将您的 class 名称作为字符串传递应该很简单。
类似问题:TypeScript class decorator get class name
GitHub 上未解决的问题:https://github.com/microsoft/TypeScript/issues/1579
"属性 已在目标上定义"
请注意,如果您 运行 上面的代码,您会在 console.log
中得到以下内容:
["constructor", "authenticate", "login", "authenticatepathInfo", "loginpathInfo"]
我还想指出,如果您甚至不初始化 class 的实例,您仍然会遇到同样的错误。
I want to read this meta data in nodejs and use that to dynamically create a client implementing the api. Basically, developers never have to write clients and only write the api and the implementation is generated for them.
如果我这样做,我可能不会使用装饰器,而是使用映射类型:
// library code
interface ApiMethodInfo {
httpPath: string;
httpMethod: string;
}
type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};
function generateClient<S extends object>(apiInfo: ApiInfo<S>): Client<S> {
const client = {} as Client<S>;
for (const key in apiInfo) {
const info = apiInfo[key as keyof S];
client[key] = ((param: any) => invokeApi(info, param)) as any;
}
return client;
}
// application code
interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
username: string,
password: string,
}
interface LoginResponse {}
interface MyServer {
authenticate(request: AuthRequest): AuthResponse;
login(request: LoginRequest): LoginResponse;
}
const myApiInfo: ApiInfo<MyServer> = { // compiler verifies that all methods of MyServer are described here
authenticate: {
httpPath: '/authenticate',
httpMethod: 'POST'
},
login: {
httpPath: '/login',
httpMethod: 'POST'
}
}
const myClient = generateClient(myApiInfo); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed
(要了解这些类型定义的工作原理,您可能需要阅读 TypeScript 手册中的 Creating Types from Types 部分)
这种方法的弱点在于,虽然编译器可以从服务器签名中导出客户端签名,但它不会复制任何 JSDoc,因此客户端开发人员无法轻松访问 API 文档。
在上面的代码中,我选择在一个单独的对象而不是装饰器中指定元数据,这样编译器可以检查详尽性(装饰器总是可选的;编译器不能被指示要求它们存在),并且因为装饰器是一项实验性语言功能,在该语言的未来版本中可能仍会发生变化。
如果您愿意,完全可以使用装饰器填充此类元数据对象。这就是它的样子:
// library code
interface ApiMethodInfo {
httpPath: string;
httpMethod: string;
}
const apiMethodInfo = Symbol("apiMethodInfo");
function api(info: ApiMethodInfo) {
return function (target: any, propertyKey: string) {
target[apiMethodInfo] = target[apiMethodInfo] || {};
target[apiMethodInfo][propertyKey] = info;
}
}
type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};
function invokeApi(info: ApiMethodInfo, param: any) {
console.log(info, param);
}
function generateClient<S extends object>(serverClass: new() => S): Client<S> {
const infos = serverClass.prototype[apiMethodInfo]; // a decorator's target is the constructor function's prototype
const client = {} as Client<S>;
for (const key in infos) { // won't encounter apiMethodInfo because Symbol properties are not enumerable
const info = infos[key];
client[key as keyof S] = ((param: any) => invokeApi(info, param)) as any;
}
return client;
}
// application code
interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
username: string,
password: string,
}
interface LoginResponse {}
class MyServer {
@api({
httpPath: '/authenticate',
httpMethod: 'POST'
})
authenticate(request: AuthRequest): AuthResponse {
throw new Error("Not implemented yet");
}
@api({
httpPath: '/login',
httpMethod: 'POST'
})
login(request: LoginRequest): LoginResponse {
throw new Error("Not implemented yet");
}
}
const myClient = generateClient(MyServer); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed
注意如何使用 Symbol 防止名称冲突,并确保其他代码看不到这个 属性(除非他们寻找那个特定的符号),因此不会被绊倒出乎意料的存在。
还要注意 MyServer
在运行时如何包含 class 的构造函数,其原型包含已声明的实例方法,并将其作为目标传递给其任何装饰器。
一般建议
我可以给恢复中的 Java 程序员一些建议作为结束吗? ;-)
EcmaScript 不是 Java。虽然语法可能看起来相似,但 EcmaScript 有许多有用的特性 Java 没有,这通常允许编写更少的代码。例如,如果您需要一个 DTO,则完全没有必要声明一个 class,构造函数将每个参数手动复制到一个 属性。您可以简单地声明一个接口,然后使用对象文字创建对象。我建议您浏览 Modern JavaScript Tutorial 以熟悉这些有用的语言功能。
此外,某些功能在 EcmaScript 中的行为有所不同。特别是,class
和 interface
之间的区别非常不同:类 用于从原型继承方法,用于传递数据的接口。为将从 JSON 反序列化的 Response
声明 class 是非常荒谬的,因为原型无法在序列化后继续存在。
我有 following code on typescript playground 并且出现了一些我不确定如何开始工作的问题
class PathInfo {
functionName: string;
httpPath: string;
httpMethod: string;
constructor(functionName: string, httpPath: string, httpMethod: string) {
this.functionName = functionName;
this.httpPath = httpPath;
this.httpMethod = httpMethod;
}
toString(): string {
return "PathInfo["+this.functionName+","+this.httpPath+","+this.httpMethod+"]";
}
}
class AuthRequest {}
class AuthResponse {}
class LoginRequest {}
class LoginResponse {}
const path: any = (thePath: string, type: any) => {
return (target: Function, memberName: string, propertyDescriptor: PropertyDescriptor) => {
const pathMeta = new PathInfo(memberName, path, type);
Object.defineProperty(target, memberName+'pathInfo', {
value: pathMeta,
writable: false
});
//How do I access the stored pathMeta
//console.log("target="+target.pathInfo);
console.log("member="+memberName);
console.log("props="+propertyDescriptor);
}
}
class AuthApiImpl {
@path("/authenticate", AuthResponse)
authenticate(request: AuthRequest): Promise<AuthResponse> {
throw new Error("all this is generated by factory.createApiImpl");
}
@path("/login", LoginResponse)
login(request: LoginRequest): Promise<LoginResponse> {
throw new Error("all this is generated by factory.createApiImpl");
}
};
function printMethods(obj: any) {
console.log("starting to print methods");
for (var id in obj) {
console.log("id="+id);
try {
//How do I access the stored pathMeta here FOR EACH METHOD ->
//console.log("target="+target.pathInfo);
if (typeof(obj[id]) == "function") {
console.log(id+":"+obj[id].toString());
}
} catch (err) {
console.log(id + ": inaccessible"+err);
}
}
}
console.log("starting to run")
const temp = new AuthApiImpl();
printMethods(temp);
console.log("done")
- 第64-65行,如何读取我设置的属性
- 40-41行,如何读取我设置的属性
- 第 58-74 行,为什么没有打印任何函数?我想打印所有函数,我不想打印属性(只是函数)
- 第 33 行,此时我可以访问 class 名称吗?
- 第 35 行,我认为 target 是一个函数并且会被授权,然后登录,但是如果我将 属性 定义为 JUST 'pathInfo',我得到一个错误 属性 已经在目标上定义(这意味着目标是 class 而不是函数?)。我很困惑。
非常抱歉,因为我试图专注于一个问题,但是当我深入打字稿世界时,这个编写装饰器的测试给了我更多的问题而不是答案。
如何调整代码以在此处播放更多内容?
这里的目标是当开发人员定义其他微服务的 API 时,我可以捕获一堆元信息并将其存储在某个地方,我可以稍后在启动代码中使用。我不在乎我真正存储它的位置,但只需要一种清晰的方式来了解我想要扩展的 class、方法、return 类型、http 路径等
如何获取 class
的方法去掉装饰器还是抢不到方法名。这不是特定于 TypeScript 的问题。
您需要获取原型的属性,而不仅仅是对象本身。
function printMethods(obj: any) {
console.log("starting to print methods");
const objProto = Object.getPrototypeOf(obj);
console.log(Object.getOwnPropertyNames(objProto));
}
如何访问 class 个名字
目前不认为装饰器可以做到这一点,但将您的 class 名称作为字符串传递应该很简单。
类似问题:TypeScript class decorator get class name
GitHub 上未解决的问题:https://github.com/microsoft/TypeScript/issues/1579
"属性 已在目标上定义"
请注意,如果您 运行 上面的代码,您会在 console.log
中得到以下内容:
["constructor", "authenticate", "login", "authenticatepathInfo", "loginpathInfo"]
我还想指出,如果您甚至不初始化 class 的实例,您仍然会遇到同样的错误。
I want to read this meta data in nodejs and use that to dynamically create a client implementing the api. Basically, developers never have to write clients and only write the api and the implementation is generated for them.
如果我这样做,我可能不会使用装饰器,而是使用映射类型:
// library code
interface ApiMethodInfo {
httpPath: string;
httpMethod: string;
}
type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};
function generateClient<S extends object>(apiInfo: ApiInfo<S>): Client<S> {
const client = {} as Client<S>;
for (const key in apiInfo) {
const info = apiInfo[key as keyof S];
client[key] = ((param: any) => invokeApi(info, param)) as any;
}
return client;
}
// application code
interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
username: string,
password: string,
}
interface LoginResponse {}
interface MyServer {
authenticate(request: AuthRequest): AuthResponse;
login(request: LoginRequest): LoginResponse;
}
const myApiInfo: ApiInfo<MyServer> = { // compiler verifies that all methods of MyServer are described here
authenticate: {
httpPath: '/authenticate',
httpMethod: 'POST'
},
login: {
httpPath: '/login',
httpMethod: 'POST'
}
}
const myClient = generateClient(myApiInfo); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed
(要了解这些类型定义的工作原理,您可能需要阅读 TypeScript 手册中的 Creating Types from Types 部分)
这种方法的弱点在于,虽然编译器可以从服务器签名中导出客户端签名,但它不会复制任何 JSDoc,因此客户端开发人员无法轻松访问 API 文档。
在上面的代码中,我选择在一个单独的对象而不是装饰器中指定元数据,这样编译器可以检查详尽性(装饰器总是可选的;编译器不能被指示要求它们存在),并且因为装饰器是一项实验性语言功能,在该语言的未来版本中可能仍会发生变化。
如果您愿意,完全可以使用装饰器填充此类元数据对象。这就是它的样子:
// library code
interface ApiMethodInfo {
httpPath: string;
httpMethod: string;
}
const apiMethodInfo = Symbol("apiMethodInfo");
function api(info: ApiMethodInfo) {
return function (target: any, propertyKey: string) {
target[apiMethodInfo] = target[apiMethodInfo] || {};
target[apiMethodInfo][propertyKey] = info;
}
}
type ApiInfo<S extends object> = Record<keyof S, ApiMethodInfo>;
type Client<S extends object> = {[key in keyof S]: S[key] extends (req: infer Req) => infer Res ? (req: Req) => Promise<Res> : never};
function invokeApi(info: ApiMethodInfo, param: any) {
console.log(info, param);
}
function generateClient<S extends object>(serverClass: new() => S): Client<S> {
const infos = serverClass.prototype[apiMethodInfo]; // a decorator's target is the constructor function's prototype
const client = {} as Client<S>;
for (const key in infos) { // won't encounter apiMethodInfo because Symbol properties are not enumerable
const info = infos[key];
client[key as keyof S] = ((param: any) => invokeApi(info, param)) as any;
}
return client;
}
// application code
interface AuthRequest {}
interface AuthResponse {}
interface LoginRequest {
username: string,
password: string,
}
interface LoginResponse {}
class MyServer {
@api({
httpPath: '/authenticate',
httpMethod: 'POST'
})
authenticate(request: AuthRequest): AuthResponse {
throw new Error("Not implemented yet");
}
@api({
httpPath: '/login',
httpMethod: 'POST'
})
login(request: LoginRequest): LoginResponse {
throw new Error("Not implemented yet");
}
}
const myClient = generateClient(MyServer); // compiler derives the method signatures from the server implementation
const username = "joe";
const password = "secret";
const response = myClient.login({username, password}); // ... and can therefore check that this call is properly typed
注意如何使用 Symbol 防止名称冲突,并确保其他代码看不到这个 属性(除非他们寻找那个特定的符号),因此不会被绊倒出乎意料的存在。
还要注意 MyServer
在运行时如何包含 class 的构造函数,其原型包含已声明的实例方法,并将其作为目标传递给其任何装饰器。
一般建议
我可以给恢复中的 Java 程序员一些建议作为结束吗? ;-)
EcmaScript 不是 Java。虽然语法可能看起来相似,但 EcmaScript 有许多有用的特性 Java 没有,这通常允许编写更少的代码。例如,如果您需要一个 DTO,则完全没有必要声明一个 class,构造函数将每个参数手动复制到一个 属性。您可以简单地声明一个接口,然后使用对象文字创建对象。我建议您浏览 Modern JavaScript Tutorial 以熟悉这些有用的语言功能。
此外,某些功能在 EcmaScript 中的行为有所不同。特别是,class
和 interface
之间的区别非常不同:类 用于从原型继承方法,用于传递数据的接口。为将从 JSON 反序列化的 Response
声明 class 是非常荒谬的,因为原型无法在序列化后继续存在。