Home > Enterprise >  Only accept keys of a given object
Only accept keys of a given object

Time:05-16

I'm trying to create a TypeScript class that is initialized with an object and has a method which can only take keys of that object as arguments. So:

class MyClass {
  properties = {};

  constructor(properties) {
    this.properties = properties;
  }

  // Passed propNames should only be keys of this.properties
  pick(...propNames) {
    return propNames.reduce((obj, name) => ({ 
      ...obj, 
      [name]: this.properties[name]
    }), {});
  }
}

This seems similar to this problem, but I can't figure out how to apply it in this case, since the properties are passed in from the outside.

const props = { key: 'value', key2: 'value2' };
interface PropKeys {
  key: string;
  key2: string;
}
type KeyName = keyof(PropKeys);

// But what do I do to the class to get this to work?
const instance = new MyClass(props);
instance.pick('key', 'key2'); // Great
instance.pick('key3'); // Should throw a type error

Is this possible? Is there a way to do it without explicitly defining the InstanceKeys, but instead deriving them from the props passed while initializing the instance?

I'm trying to wrap my head around generics, and was thinking maybe something like this:

class MyClass {
  properties = {};

  constructor<Type>(properties: Type) {
    this.properties = properties;
    type TypeKeys = keyof(Type);
  }
  
  pick(...propNames: TypeKeys[]) {
    return propNames.reduce((obj, name) => ({ 
      ...obj, 
      [name]: this.properties[name]
    }), {});
  }
}

But this raises two type errors:

  • On the <Type>: "Type parameters cannot appear on a constructor declaration."
  • On the TypeKeys[]: "Cannot find name 'TypeKeys'." (I mean, makes sense; it's out of scope.)

UPDATE: This feels closer, but I'm running into an issue where the properties are first defined on the class (above the constructor):

class MyClass<PropType extends Properties> {
  properties: PropType = {};

  constructor(properties: PropType) {
    this.properties = properties;
  }

  pick(...propNames: Array<keyof(PropType)>) {
    return propNames.reduce((obj, name) => ({ 
      ...obj, 
      [name]: this.properties[name]
    }), {});
  }
}

The TS error I'm running into on that line is

Type '{}' is not assignable to type 'PropType'. '{}' is assignable to the constraint of type 'PropType', but 'PropType' could be instantiated with a different subtype of constraint 'Properties'

The issue here is that any properties passed in may have their own keys, but must be instances of type Properties, which constrains the values.

CodePudding user response:

Your generic type needs to go on the class declaration, not on its constructor. Then keyof Type needs to be an anonymous type. You also need to type properties so TypeScript knows it can be indexed with keyof Type, which I've done in this example by giving it a type of Partial<Type>.

I've also used a type assertion so that the initial {} object of your reduce is typed as Partial<Type>, so TypeScript will understand how to index it after it's been created.

class MyClass<Type> {
  properties: Partial<Type> = {};

  constructor(properties: Type) {
    this.properties = properties;
  }
  
  pick(...propNames: (keyof Type)[]) {
    return propNames.reduce((obj, name) => ({ 
      ...obj, 
      [name]: this.properties[name]
    }), {} as Partial<Type>);
  }
}

TypeScript Playground

  • Related