【问题标题】:Typescript genericized Omit?打字稿泛化省略?
【发布时间】:2022-11-22 05:14:45
【问题描述】:

我正在尝试围绕 prisma 数据库模型创建通用包装器。该模型只是一个类型化对象,表示要返回的数据库表行。你可以这样想:

type User = {
  user_id: bigint;
  password: string;
  email_address: string;
}

包装器围绕这些模型提供了一堆实用函数,看起来像这样:

    export default class Entity<T extends {}> {
    private readonly cleanModel: T;
    private model: Partial<T>| T;

    constructor(
        model: T,
        guardedProps: string[],
    ) {
        this.cleanModel = model;

        // By default, hide guarded props. Guarded props are only accessible
        // through methods which acknowledge guarded status
        const withoutSensitive: Partial<T> = _.omit(model, guardedProps);
        this.model = withoutSensitive;
    }

    /**
     * Returns the value of the provided key from the model.
     * @param key 
     */
    prop(key: keyof T): any {
        if (key in this.model) {
            return this.model[key];
        }

        throw TypeError(`Key ${String(key)} does not exist on entity Model`);
    }

    guardedProp(key: keyof T): any {
        if (key in this.cleanModel) {
            return this.cleanModel[key];
        }

        throw TypeError(`Key ${String(key)} does not exist on entity Model`);
    }

    /**
     * Picks just the requested keys and returns a new object with those keys.
     * To grab guarded properties, the boolean withGuarded can be passed in.
     * @param props 
     * @param withGuarded 
     * @returns 
     */
    pick(props: (keyof T)[], withGuarded: boolean = false): Partial<T> {
        let picked: Partial<T>  = _.pick(withGuarded ? this.cleanModel : this.model, props);
        return picked;
    }

    toString(): string {
        return this.model.toString();
    }

    toJSON(): Partial<T> | T {
        return this.model;
    }

}

注意 model 和 guardedProps 都是 Partial 类型。相反,我更愿意做的是让 model 和 guardedProps 都是 Omit 类型,这样我就不必处理 Partial 的可选性质。这将提高 IDE 的完成度,并​​且有助于使敏感信息(例如用户密码)不会在日志或 API 响应中意外泄露。

但是,我似乎无法找到一种方法来为 Entity 提供通用的键联合。我愿意为每个模型的每个联合定义类型,但我找不到通用化的方法任何一个。

有什么方法可以在类上定义一个属性,该属性被键入为键的联合,并且可以像 Omit&lt;T, T["protectedProps"] 一样被接受为 Omit 中的参数?我试过 protectedProps: (keyof User)[] = ['password', 'user_id'] 解决得很好,但在实体中导致错误,因为当我尝试前面提到的 Omit 语法时,keyof T[] 不可分配给类型 keyof T

【问题讨论】:

    标签: typescript generics prisma


    【解决方案1】:

    我想你正在寻找这个。

    class Entity<T, Garded extends string> {
        private readonly cleanModel: T;
        private model: _._Omit<T, Garded>;
    
        constructor(model: T, guardedProps: Garded[]) {
            this.cleanModel = model;
    
            // By default, hide guarded props. Guarded props are only accessible
            // through methods which acknowledge guarded status
            const withoutSensitive = _.omit(model, guardedProps);
            this.model = withoutSensitive;
        }
    
        /**
         * Returns the value of the provided key from the model.
         * @param key 
         */
        prop<K extends keyof _._Omit<T, Garded>>(key: K): _._Omit<T, Garded>[K] {
            if (key in this.model) {
                return this.model[key];
            }
    
            throw TypeError(`Key ${String(key)} does not exist on entity Model`);
        }
    
        guardedProp<K extends keyof T>(key: K): T[K] {
            if (key in this.cleanModel) {
                return this.cleanModel[key];
            }
    
            throw TypeError(`Key ${String(key)} does not exist on entity Model`);
        }
    
        /**
         * Picks just the requested keys and returns a new object with those keys.
         * To grab guarded properties, the boolean withGuarded can be passed in.
         * @param props 
         * @param withGuarded 
         * @returns 
         */
        pick<K extends keyof T & string>(props: K[], withGuarded: true): _._Pick<T, K>
        pick<K extends keyof T & string>(props: K[], withGuarded?: false): _._Pick<_._Omit<T, Garded>, K>
        pick<K extends keyof T & string>(props: K[], withGuarded: boolean = false) {
            return _.pick(withGuarded ? this.cleanModel : this.model, props);
        }
    
        toString(): string {
            return this.model.toString();
        }
    
        toJSON(): _._Omit<T, Garded> {
            return this.model;
        }
    
    }
    

    下划线有自己的 _Pick_Omit 类型。我没有费心找出区别,但它们似乎与标准实用程序类型不兼容,所以你有点被迫使用它们。

    我注意到 pick 的一种返回类型中有 Partial 。我想你可以安全地关闭它,因为 Entity 是详尽无遗的。

    【讨论】:

    • 是的! Guarded extends string 正是我要找的。就表示类型中的键联合而言。但是,我的方法还有更多问题。出于某种原因,我在 lodash 中找不到 _Pick 类型。所以我无法将省略的属性准确地输入到它们自己的变量中。所以我完全改变了我的方法。
    • 实体现在有一个看起来像这样的签名:class Entity&lt;T extends {}, GuardedT extends {}, TGuarded extends {}&gt;,其中 GuardedT 是 Omit&lt;User, "password"&gt;,在我的示例中 TGuarded 是 Pick&lt;User, "password"&gt;。此外,我的构造函数采用一个名为 guardedProps 的 keyof T 数组,它是在每个模型上定义的。在实例化过程中,我将该值与基类型一起用于将值分离为受保护的和安全的属性。我将在下面发布代码。
    • 我接受了你的回答,因为你确实回答了最初的问题,即如何将一个类型的键联合表示为一个类型。这最终使我找到了我现在正在使用的解决方案。
    • 哦,我认为 _Pick 的问题是我假设你正在使用下划线并不是lodash.您应该在原始问题中将其添加为标签和/或显式导入。
    【解决方案2】:

    看到杰弗里的回答后,我玩了几个小时。虽然将省略的键添加到类中对于键入很有用,但它只为我提供了我需要的类型,并且在运行或编译时没有足够的信息来帮助我缩小类型范围。

    理想情况下,我也不想每次需要实体实例时都传入大量类型。所以这就是我最后的结果,以防有人想自己做这样的事情。

    用户模型

    type User = {
      user_id: bigint;
      password: string;
      email_address: string;
    }
    

    模型类(替代Entity),作为要扩展的基类。

    export class Model<T extends {}, GuardedT extends {}, TGuarded extends {}> {
        private cleanModel: T;
    
        model: GuardedT;
        guarded: TGuarded;
    
        constructor(model: T, guardedProps: (keyof T)[]) {
            // Create a clean version of the prisma model as it is now. This will help
            // determine if changes are made later. 
            this.cleanModel = _.cloneDeep(model);
    
            // Separate the guarded props from the rest.
            // Any is used here because we will gradually build up to the expected
            // types.
            const props: any = {};
            const guarded: any = {};
    
            for (const prop in model) {
                if (guardedProps.includes(prop)) {
                    guarded[prop] = model[prop];
                } else {
                    props[prop] = model[prop];
                }
            }
    
            this.model = props as GuardedT;
            this.guarded = guarded as TGuarded;
        }
    
        toJSON(): GuardedT {
            return this.model;
        }
    
        toString(): string {
            return this.model.toString();
        }
    
        /**
         * Picks just the requested keys and returns a new object with those keys.
         * Should be used where possible for network transmission of model data.
         * @param props 
         * @returns 
         */
        pick(props: (keyof T)[]): Partial<T> {
            return _.pick(this.cleanModel, props);
        }
        
    }
    

    要使用它,需要定义一个新类来表示基本模型,就像这样。我还定义了代表受保护模型的类型,以及受保护的道具。

    export type UserGuard = Omit<User, "password">;
    export type UserGuarded = Pick<User, "password">;
    
    const guarded: (keyof User)[] = ['password'];
    
    export class UserModel extends Model<User, UserGuard, UserGuarded>{
        constructor(model: User) {
            super(model, guarded);
            this.model = model;
        }
    }
    

    现在我可以简单地类new UserModel(returnedModelFromPrisma)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-19
      • 1970-01-01
      • 2015-11-27
      • 2020-03-07
      • 1970-01-01
      相关资源
      最近更新 更多