1

I am trying to create a class and methods that will be used to retrieve data from database. For instance:

export default abstract class BaseRepository<T> {
  public async findEntities(conditionFieldsObj: Partial<T>): Promise<T[]> 

and call is:

table1.findEntities( { column1filter: "something" })

this will return all fields for the table generating select * from table where column1filter = "something" and the return type will be <T>

this works fine

I also want to have additional method to be able to restrict list of columns and return Partial type so I do:

export default abstract class BaseRepository<T> {
  async findEntitiesPartial<D extends Partial<T>>(conditionFieldsObj: Partial<D>, findFields: Array<keyof D>): Promise<D[]> {}

and call is:

type  tablePartial = Pick<tableEntity, "column1" | "column2" > 

const result = await table1.findEntitiesPartial<tablePartial>( { column1filter: "something" }, ["column1", "column2"]) 

findFields: Array<keyof D> helps to prevent adding properties that do not exist in tablePartial and in tableEntity type and have a list of columns to be queried but I have the following issues:

  1. I can skip a column in findFields which will lead to an error, all columns should be listed to return the proper tablePartial Is there a way so in findFields all properties must be listed

  2. Is there a way to not repeat twice columns in type tablePartial and findFields, otherwise I have to list those twice which is ugly. As far as I understood I cannot get type properties in runtime but maybe there is a way to somehow do this scenario nicely.

Thanks

3
  • Partial<D> should have no useful effect because D is already extending Partial<T>? And should it be { column1: "something" } instead? Or is it always going to be like someColumnfilter where there's always "filter" appended to the column name? Commented Aug 16, 2022 at 14:44
  • yes, i removed it, still the same. I can do await table1.findEntitiesPartial<tablePartial>( { column1filter: "something" }, ["column1", ~~"column2~~"]) removing column2 which i want to avoid. And { column1: "something" } is just a filter not relevant for case. Filter fields are optional so I can select 3 fields but only one is filtered Commented Aug 16, 2022 at 15:04
  • actually it is needed, extends Partial<T> should be there because I want that only properties (only existing columns of table) of type <T> to be used not some arbitrary. Commented Aug 16, 2022 at 15:14

1 Answer 1

2

I don't know if I've understood you correctly but here is a solution wehre you don't need to specify the ´´´findfields```- property twice but you can to make it more explicit

export default abstract class BaseRepository<T> {
  public async findEntities(conditionFieldsObj: Partial<T>): Promise<T[]> {
    throw Error("not implemented")
  }
  public async findEntitiesPartial<F extends (keyof T)[]>(conditionFieldsObj: Partial<T>, findFields: F): Promise<Pick<T, F[number]>[]> {
    throw Error("not implemented")
  }

}

class Table1 extends BaseRepository<TableEntity>{

}

type TableEntity = { column1: string, column2: string, column3: string }

type tablePartial = Pick<TableEntity, "column1" | "column2">

const table1 = new Table1()
// infered restriction
const result = (await table1.findEntitiesPartial({ column1: "something", column2: "a" }, ["column1", "column2",])) //Pick<TableEntity, "column1" | "column2">[]
const result1 = (await table1.findEntitiesPartial({ column1: "something", column2: "a" }, ["column1", "column2", "column4"])) //error
// restrict by generic
const result2 = (await table1.findEntitiesPartial<["column1", "column2"]>({ column1: "something", column2: "a" }, ["column1", "column2", "column3"])) //error
const result3 = (await table1.findEntitiesPartial<["column1", "column3"]>({ column1: "something", column2: "a" }, ["column1", "column3"])) ///Pick<TableEntity, "column1" | "column3">[]


Edit:


// So how does work
export default abstract class BaseRepository<T> {
  public async findEntitiesPartial<F extends (keyof T)[]>(conditionFieldsObj: Partial<T>, findFields: F): Promise<Pick<T, F[number]>[]> {
    throw Error("not implemented")
  }
}
// We restrict our generic to be an array of keys of our table
// F could be ["column1","column2","column2"] or "column1"[]
// but it could never be ["something else"]
// Now it's important to know that a type constraint F extends (keyof T)[] (everything after F) 
// is no type assignment nothing will get infered by this constraint. 
// It's just a placeholder that's restricted.
// You can assign a value to that placeholder by using the generic or by type inference.
function test<T>(input:T):T{
  return input
}
const a = test("asd") //"asd"
const b = test<"asd">("") //Argument of type '""' is not assignable to parameter of type '"asd"'

// Ok,now we explicitly tell typescript which value to use e.g.:["column1","column2","column2"]
// let's replace F with our example and let's get rid of our generic F and replace T with our TableEntity
type TableEntity = { column1: string, column2: string, column3: string }
export default abstract class BaseRepository2 {
  public async findEntitiesPartial(
    conditionFieldsObj: Partial<TableEntity>,
    findFields: ["column1", "column2", "column2"]):
    Promise<Pick<TableEntity, ["column1", "column2", "column2"][number]>[]> {
    throw Error("not implemented")
  }
}
// ["column1","column2","column2"][number] will resolve to "column1"|"column2"|"column2" which is widened to "column1"|"column2"
export default abstract class BaseRepository2 {
  public async findEntitiesPartial(
    conditionFieldsObj: Partial<TableEntity>,
    findFields: ["column1", "column2", "column2"]):
    Promise<Pick<TableEntity, "column1" | "column2">[]> {
    throw Error("not implemented")
  }
}

playground

Sign up to request clarification or add additional context in comments.

4 Comments

it is exactly what I wanted. Thanks a lot. The only issue now for me personally is to understand how all this works and how to come up with something like this especially ` Promise<Pick<T, F[number]>[]>`
Well, let me try to explain it to you. You first have to think about what you want to achieve exactly. And it's nothing more than pick some keys from an known object. So your function needs a list to identify all keys that it need to pick, thats your "findFieldList". You made the mistake to not restrict these values to a specific input because you used Array<keyof D> which will resolve to ("colum1"|"column2"|"column3")[]. There is no restriction. You are allowing any column key.
You will achieve this restriction by using a tuple and the type constraint (keyof T)[]. A tuple is an array with an known length and typescript is infering the values of the tuples and that's basicly it.
By adding the type constraint (keyof T)[] every key of your table is allowed as input for the generic but you don't have to use all of them. When you now using the generic with a tuple like ["column1"]F will be ["column1"] and not (keyof T)[] anymore. therefore findfields has to be ["column1"] as well. And the last part the return type: By infering the tuple we can access the union of the used keys, by indexing our tuple with number e.g. type Tup = ["column1","column2"][number] -> "column1"|"column2" we can use that for the second argument for Pick.I will add a better explanation later

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.