简体   繁体   中英

Create Reusable Functions with Typescript

I have a function that returns other functions like so:

export const makeAudienceDb = () => {
  async function insert({ ...params }: AudienceAttributes) {
    const audience = await AudienceModel.create({ ...params })
    const audienceToJson = audience.toJSON()
    return audienceToJson
  }

  async function findById({ id }: { id: number }) {
    const user = await AudienceModel.findByPk(id)
    return user?.toJSON()
  }

  async function remove({ id }: { id: number }) {
    return AudienceModel.destroy({ where: { id } })
  }

  async function update({
    id,
    ...changes
  }: { id: number } & AudienceAttributes) {
    const updated = await AudienceModel.update(
      { ...changes },
      { where: { id } }
    )
    return updated
  }

  return Object.freeze({
    insert,
    findById,
    remove,
    update,
  })
}

I have other models, eg UserModel , PostModel which have the same database operations as makeAudienceDb . eg

export const makeUsersDb = ({ hashPassword, createToken }: DBDeps) => {
  async function insert({ ...params }: User) {
    if (params.password) {
      params.password = await hashPassword(params.password)
    }
    const newUser = await UserModel.create({ ...params })
    const returnedUser = newUser.toJSON()

    const { id } = newUser
    const { name, username, email, password } = returnedUser as User
    const payload = {
      id,
      email,
    }
    const token = createToken(payload)
    const user = { id, name, username, password, email }
    return { user, token }
  }

  async function findById({ id }: { id: number }) {
    const user = await UserModel.findByPk(id)
    return user?.toJSON()
  }

  async function findByEmail({ email }: { email: string }) {
    const user = await UserModel.findOne({ where: { email } })
    return user?.toJSON()
  }

  async function remove({ id }: { id: number }) {
    return UserModel.destroy({ where: { id } })
  }

  async function update({ id, ...changes }: { id: number } & User) {
    const updated = await UserModel.update({ ...changes }, { where: { id } })
    return updated
  }

  return Object.freeze({
    insert,
    findByEmail,
    findById,
    remove,
    update,
  })
}

This leads to code duplication across various levels. I want to know how to create one major function that I can reuse for other database operations. So instead of me having makeAudienceDb , makeUsersDb , I could just have one major function eg majorDatabaseOps where makeAudienceDb and makeUsersDb could just inherit from. I know this is possible with ES6 Classes and Interfaces but I was just wondering how I could implement the same in a functional way. Any contribution is welcome. Thank you very much!

It looks like what you are trying to do is bind shortcuts to particular sequelize methods. The shared functionality can be implemented using typescript generics. Overriding specific behaviors, like hashing a password for a User makes this a bit more complex.

My first instinct is to use a class-based approach. But you can do it with functions by copying all of the methods from the base and then overriding or adding specific ones, along these lines:

const userDb = Object.freeze({
  ...makeDb(UserModel), 
  findByEmail: async ({email}: {email: string}) => {
  }
})

We want to create a function that takes the model as an argument. It will use typescript generics to describe the types associated with that model. The generics will be inferred from the model variable when calling the function.

A sequelize Model has two generic values: TModelAttributes and TCreationAttributes which is optional and defaults to TModelAttributes . We also want to require that all of our model attributes must include {id: number} .

You could potentially add additional typings to get better support for toJSON . The sequelize package just declares the return type from Model.toJSON as object which is vague and unhelpful.

Our general function looks like this:

import {Model, ModelCtor} from "sequelize";

export const makeDb = <TModelAttributes extends {id: number} = any, TCreationAttributes extends {} = TModelAttributes>(
    model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>
  ) => {
  async function insert({ ...params }: TCreationAttributes) {
    const created = await model.create({ ...params });
    return created.toJSON();
  }

  async function findById({ id }: { id: number }) {
    const found = await model.findByPk(id)
    return found?.toJSON()
  }

  async function remove({ id }: { id: number }) {
    return model.destroy({ where: { id } })
  }

  async function update({ ...changes }: { id: number } & Partial<TModelAttributes>) {
    const { id } = changes; // I get a TS error when destructuring this in the args
    const updated = await model.update(
      { ...changes },
      { where: { id } }
    );
    return updated;
  }

  return Object.freeze({
    insert,
    findById,
    remove,
    update,
  });
}

For audience, you would simply call:

const audienceDb = makeDb(AudienceModel);

Or you could define it as a function if you wanted to:

const makeAudienceDb = () => makeDb(AudienceModel);

For the users database, we need to override insert , add findByEmail , and take additional arguments hashPassword and createToken .

This is not elegant, but it should work. I don't love that your return type for user insert is incompatible with the insert returned value from the general makeDb .

export const makeUsersDb = ({ hashPassword, createToken }: DBDeps) => {

  // declaring this up top so that you could call methods on it in your overrides
  const db = makeDb(UserModel);

  async function insert({ ...params }: User) {
    if (params.password) {
      params.password = await hashPassword(params.password)
    }
    const newUser = await UserModel.create({ ...params })
    const returnedUser = newUser.toJSON()

    const { id } = newUser
    const { name, username, email, password } = returnedUser as User
    const payload = {
      id,
      email,
    }
    const token = createToken(payload)
    const user = { id, name, username, password, email }
    return { user, token }
  }

  async function findByEmail({ email }: { email: string }) {
    const user = await UserModel.findOne({ where: { email } })
    return user?.toJSON()
  }

  return Object.freeze({
    ...db,
    insert,
    findByEmail
  })
}

How about defining the common functions outside of the majorDatabaseOps function? Will let you reuse them in different places.

 //defined outside for reusability function findById(model, id) { return model.findByPk(id) } const majorDatabaseOps = model => { function removeById(model, id) { return model.remove(id); } return Object.freeze({ removeById: id => removeById(model, id), findById: id => findById(model, id), }) } //mocking models for demonstration const UserModel = { modelName: "UserModel", findByPk: function(id) { return console.log(id + " was found in " + this.modelName) }, remove: function(id) { return console.log(id + " was removed from " + this.modelName) } } const PostModel = { modelName: "PostModel", findByPk: function(id) { return console.log(id + " was found in " + this.modelName) }, remove: function(id) { return console.log(id + " was removed from " + this.modelName) } } const userDbOps = majorDatabaseOps(UserModel); userDbOps.findById(1); userDbOps.removeById(1); const postDbOps = majorDatabaseOps(PostModel); postDbOps.findById(2); postDbOps.removeById(2);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM