TypeScript 缩短通用函数调用

Posted

技术标签:

【中文标题】TypeScript 缩短通用函数调用【英文标题】:TypeScript shorten generic function call 【发布时间】:2021-11-18 16:52:29 【问题描述】:

我在节点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;
;

如您所见,flag virtuals 的代码基本上是重复的(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 智能感知。但是它知道直接访问this.flags是一个number

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

【问题讨论】:

【参考方案1】:

我自己想通了

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&lt;Flags, &gt; &amp; Flags = Model&lt;Flags, &gt; &amp; Flags

Model&lt;Flags, &gt;this.getDataValue 知道'flags' 是一个允许的值,&amp; Flags 让 typescript 知道直接this.flags 访问是number

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

【讨论】:

以上是关于TypeScript 缩短通用函数调用的主要内容,如果未能解决你的问题,请参考以下文章

Typescript:如何使用回调调用 Javascript 函数

在构造函数之前调用 Typescript/javascript 方法

如何在 TypeScript 中将函数作为变量调用 [关闭]

如何测试使用 jasmine + TypeScript 使用常量调用的函数

不支持 Typescript 2.1.5 函数调用

Typescript 从具有泛型类型的对象索引调用函数