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