SoWhat
SoWhat

Reputation: 5622

How do I create an object type that should have all the keys of an array type

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

Answers (2)

Tyler Sebastian
Tyler Sebastian

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

TimTIM Wong
TimTIM Wong

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

Related Questions