Reputation: 5622
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
}
}
Upvotes: 0
Views: 223
Reputation: 9468
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).
Upvotes: 1
Reputation: 818
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.
Upvotes: 0