使用 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")

非常抱歉,因为我试图专注于一个问题,但是当我深入打字稿世界时,这个编写装饰器的测试给了我更多的问题而不是答案。

如何调整代码以在此处播放更多内容?

这里的目标是当开发人员定义其他微服务的 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 中的行为有所不同。特别是,classinterface 之间的区别非常不同:类 用于从原型继承方法,用于传递数据的接口。为将从 JSON 反序列化的 Response 声明 class 是非常荒谬的,因为原型无法在序列化后继续存在。