I am using Sequelize 6.6.5 with TypeScript 4.4.3 on Node 14.17.6
db/models/User.ts (only relevant parts)
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;
};
As you can see, the code for the flag virtuals is basically duplicated (WET).
I wanted to have something like this
isDev: createFlagVirtual<IUserFlags>(flagPositions, 'isDev');
After a long list of errors, I came up with
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);
},
};
}
Wondering how I call it?:
createFlagVirtual<IUserAttributes, IUserCreationAttributes, User, IUserFlags>(
flagPositions,
'isMod',
);
Yah; not so DRY.
On top of which, typescript still has no idea that 'flags' can be passed to this.getDataValue, atleast according to vscode intellisense. However, it knows that directly accessing this.flags is a number.
I want to know how to shorten the call to createFlagVirtual so that the following works createFlagVirtual<IUserFlags>(flagPositions, 'isDev');. TypeScript should know what can be passed to the second argument, aka a keyof IUserFlags. Also, in the implementation, this.getDataValue should know that 'flags' is a value.
Comments
Post a Comment