TypeScript 缩短泛型函数调用

TypeScript shorten generic function call

我在节点 14.17.6

上使用 Sequelize 6.6.5TypeScript 4.4.3

db/models/User.ts(仅相关部分)

import {
  Sequelize,
  Model,
  DataTypes,
  Optional,
  ModelAttributeColumnOptions,
} from 'sequelize';

import { FlagPositions, Flags } from '../types';

export interface IUserFlags {
  isDev: boolean;
  isAdmin: boolean;
  isMod: boolean;
}

export const flagPositions: FlagPositions<IUserFlags> = {
  isDev: 0,
  isAdmin: 1,
  isMod: 2,
};

export interface IUserAttributes extends Flags, IUserFlags {
  id: number;

  name: string;

  email: string;

  // ...
}

export interface IUserCreationAttributes
  extends Optional<
    IUserAttributes,
    | 'id'
    | 'flags'
    | 'isDev'
    | 'isAdmin'
    | 'isMod'
  > {}

export class User
  extends Model<IUserAttributes, IUserCreationAttributes>
  implements IUserAttributes
{
  public id!: number;

  public name!: string;

  public email!: string;

  public flags!: number;

  public isDev!: boolean;
  public isAdmin!: boolean;
  public isMod!: boolean;

  // ...
}

export function init(sequelize: Sequelize): void {
  User.init(
    {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
        unique: true,
      },
      name: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true,
      },
      email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true,
      },
      // ...
      flags: {
        type: DataTypes.INTEGER,
        defaultValue: 0,
      },
      isDev: {
        type: DataTypes.VIRTUAL,
        get(): boolean {
          return !!((this.flags >> flagPositions.isDev) & 1);
        },
        set(value: boolean): void {
          const flags = this.getDataValue('flags');

          const mask = 1 << flagPositions.isDev;

          this.setDataValue('flags', value ? flags | mask : flags & ~mask);
        },
      },
      isAdmin: {
        type: DataTypes.VIRTUAL,
        get(): boolean {
          return !!((this.flags >> flagPositions.isAdmin) & 1);
        },
        set(value: boolean): void {
          const flags = this.getDataValue('flags');

          const mask = 1 << flagPositions.isAdmin;

          this.setDataValue('flags', value ? flags | mask : flags & ~mask);
        },
      },
      isMod: {
        type: DataTypes.VIRTUAL,
        get(): boolean {
          return !!((this.flags >> flagPositions.isMod) & 1);
        },
        set(value: boolean): void {
          const flags = this.getDataValue('flags');

          const mask = 1 << flagPositions.isMod;

          this.setDataValue('flags', value ? flags | mask : flags & ~mask);
        },
      },
    },
    { sequelize, tableName: 'users' },
  );
}

../types

interface Flags {
  flags: number;
}

type FlagPositions<F extends Record<keyof F, boolean>> = {
  [K in keyof F]: number;
};

如您所见,标记虚拟的代码基本上是重复的 (WET)。

我想要这样的东西

isDev: createFlagVirtual<IUserFlags>(flagPositions, 'isDev');

经过一长串的错误,我想到了

function createFlagVirtual<
  TModelAttributes extends Flags,
  TModelCreationAttributes extends Partial<TModelAttributes>,
  TModel extends Model<TModelAttributes, TModelCreationAttributes> &
    TModelAttributes,
  TFlags extends Record<keyof TFlags, boolean>,
  TFlag extends keyof TFlags = keyof TFlags,
>(
  positions: FlagPositions<TFlags>,
  flag: TFlag,
): ModelAttributeColumnOptions<TModel> {
  return {
    type: DataTypes.VIRTUAL,
    get(): boolean {
      return !!((this.flags >> positions[flag]) & 1);
    },
    set(value: boolean): void {
      const flags = this.getDataValue('flags');

      const mask = 1 << positions[flag];

      this.setDataValue('flags', value ? flags | mask : flags & ~mask);
    },
  };
}

想知道我怎么称呼它吗?:

createFlagVirtual<IUserAttributes, IUserCreationAttributes, User, IUserFlags>(
  flagPositions,
  'isMod',
);

是的;没那么干。

最重要的是,typescript 仍然不知道 'flags' 可以传递给 this.getDataValue,至少根据 vscode intellisense。但是,它知道直接访问 this.flagsnumber.

我想知道如何缩短对 createFlagVirtual 的调用,以便下面的工作 createFlagVirtual<IUserFlags>(flagPositions, 'isDev');。 TypeScript 应该知道什么可以传递给第二个参数,也就是 keyof IUserFlags。此外,在实现中,this.getDataValue 应该知道 'flags' 是一个值。

我自己想出来的

function createFlagVirtual<
  TFlags extends Record<keyof TFlags, boolean>,
  TModel extends Model<Flags, {}> & Flags = Model<Flags, {}> & Flags,
>(
  positions: FlagPositions<TFlags>,
  name: keyof TFlags,
): ModelAttributeColumnOptions<TModel> {
  return {
    type: DataTypes.VIRTUAL,
    get(): boolean {
      return !!((this.getDataValue('flags') >> positions[name]) & 1);
    },
    set(value: boolean): void {
      const flags = this.getDataValue('flags');

      const mask = 1 << positions[name];

      this.setDataValue('flags', value ? flags | mask : flags & ~mask);
    },
  };
}

用作

createFlagVirtual<IUserFlags>(flagPositions, 'isDev')
// or
createFlagVirtual<IUserFlags, User>(flagPositions, 'isDev'),

关键部分是TModel extends Model<Flags, {}> & Flags = Model<Flags, {}> & Flags

Model<Flags, {}> 允许 this.getDataValue 知道 'flags' 是允许的值,而 & Flags 让打字稿知道直接 this.flags 访问是number.

编辑:这显示了 TypeScript 可能是一团糟,以及它如何通过让程序员专注于最终无关紧要的事情来实际减慢开发速度。我再次将我的代码库移至原版 javascript。