0

I am trying to create a generic class to define a resource which has crud operations. My end goal is to have a generic class instance that can be used to create services, reducer slices and components to automatically display data, filter and paginate a particular resource. For the current use case this is what I want to define

Resource City has parents state and country CityInstance.getPath should accept ids for state and country and return /country/1/states/1/cities

export default class Resource {
  name: string;
  path: string;
  parents: string[];

  constructor(arg: { name: string; path: string; parents: string[] }) {
    this.name = arg.name;
    this.path = arg.path;
    this.parents = arg.parents;
  }
//I want the argument parentIds to include every single element of parents and their ids
  getBaseUrl(parentIds: {[parentName:string]=> id}){
       const parentPath = this.parents.map(p=>`/${p}/${parentIds[p]}`).join("");

       return `${parentPath}/${this.path}`
  }

}


const resource = new Resource({name:"city", parents: ["countries", "states"]})
//The next call should force me to supply the object of ids  and should not allow calls without them such as this one

resource.getBaseUrl({}) // Typescript should not allow


resource.getBaseUrl({country: 1}) // Typescript should not allow. 


//This would be valid since it supplies both state and country

resource.getBaseUrl({country: 1, state:2});

I realize that typescript cannot predict what the runtime values would be and therefore cannot infer types from the code alone. I have tried creating parents as a type but don't know how to work with that

class Resource<ResourceType,  Parents extends string[] =[] > {
    name:string
    resourceUrlName:string
    
    constructor(name:string,resourceUrlName:string){
        this.name=name
        this.resourceUrlName = resourceUrlName
    }
    //How do I specify that the indexer should be a part of parents array type
    generateBasePathForCollection(parents: {[name: keyof Parents} : number}){
      // How do I get all the members of the Parents Array

    }
}

2 Answers 2

1

As far as I know, you can't do mapped types with the values of an array. What about changing the signature a little bit to help you? You can do a mapped type of object keys.

type WithParents<T extends Record<string, number>> = {
  baseUrl?: string;
  name: string;
  parents: T;
};

class Resource<T extends Record<string, number>> {
  baseUrl?: string;
  name: string;
  parents: T;

  constructor(args: WithParents<T>) {
    this.baseUrl = args.baseUrl;
    this.name = args.name;
    this.parents = args.parents;
  }

  getBaseUrl(input: { [P in keyof T]: string | number }) {
    const parts = Object.keys(this.parents).sort(k => this.parents[k]);
    return [this.baseUrl, ...parts.map(key => `${key}/${input[key]}`)]
      .filter(Boolean)
      .join("/");
  }
}

The important parts here are the generics on the class and the WithParents type, and the mapped type arg (based on the generic argument on the class) in getBaseUrl. Here are the docs on mapped types: https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types

Then

const x = new Resource({ name: "string", parents: { countries: 1, state: 2 } });

console.log(x.getBaseUrl({ countries: 1, state: 2 })); // countries/1/state/2 
console.log(x.getBaseUrl({ state: 1, countries: 2 })); // countries/2/state/1 

const y = new Resource({ name: "string", parents: { state: 1, countries: 2 } });

console.log(y.getBaseUrl({ countries: 1, state: 2 })); // state/2/countries/1 
console.log(y.getBaseUrl({ state: 1, countries: 2 })); // state/1/countries/2 

You could make it useful, like { countries: "number" } and then do some validation on that while you're in there. As it stands, you can put any value for the value of the entry in the record and it'll work.

Edit: updated to maintain an ordering. Since the initial values for parents aren't used, we'll use their value to signify an order (starting from 1).

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

8 Comments

Right, arrays don't have keys. An array only has values that may be statically typed, but not for dynamic values.
The only problem is that the order of the parents matters. This could produce countries/1/state/1/cities or state/1/countries/2/cities
I am going to test this out and get back to you. I did think of using record but thought the ordering might be a problem
This is actually quite the novel solution and yes it does appear to work. repl.it/@slaith/DismalRaggedProcess#index.ts I think the problem is the reliance on the order of stuff. Perhaps there's a way to remove that
Avoid using any as it should only be either number or string in an URL, or something that implements toString(): string or valueOf(): number.
|
0

Updated to keep parents

parents is provided as an object of the same type of parentIds as T. The values of parents do not matter except their types. The order of the keys is preserved.

export default class Resource<T extends { [parentName: string]: number }> {
    name: string;
    path: string;
    parents: T;

    constructor({ name, path, parents }: { name: string; path: string; parents: T; }) {
        this.name = name;
        this.path = path;
        this.parents = parents;
    }
    // You want the argument parentIds to include every single element of parents and their ids
    getBaseUrl(parentIds: T) {
        const parentPath = Object.keys(this.parents).map(p => `/${p}/${parentIds[p]}`).join("");

        return `${parentPath}/${this.path}`
    }

}


const resource = new Resource({ name: "city", path: "dont-forget-path", parents: { country: 0, state: 0 } })
//The next call should force me to supply the object of ids  and should not allow calls without them such as this one

resource.getBaseUrl({}) // Typescript should not allow


resource.getBaseUrl({ country: 1 }) // Typescript should not allow. 


//This would be valid since it supplies both state and country

resource.getBaseUrl({ country: 1, state: 2 });

Note that {[parentName:string]=> id} is invalid as => should be : and id must be a type instead.

Moreover, don't forget to put path when calling the constructor.

1 Comment

The only problem is that the order of the parents matters. This could produce countries/1/state/1/cities or state/1/countries/2/cities

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.