typescriptooptypesinterfacedynamically-generated

How do I refactor (fix) this method?


I'm new with typescript and I kept getting the same "not assignable to parameter of type error" error, and, after some research, I solve it but I'm not proud of the solution.

I'm working with a simple class and to simplify I let only the attribute I'm working with.

interface Proficiencies {
  armor: string[];
  weapons: string[];
  tools: string[];
  savingThrows: number[];
  skills: boolean[];
}

interface ProfAndBonus extends Proficiencies {
  bonus: number
}
class Character {
  public readonly proficiencies: ProfAndBonus = {
      bonus: 2,
      armor: [],
      weapons: [],
      tools: [],
      savingThrows: [],
      skills: []
    }

// This is the code that have the error
public addProficiency<T extends keyof Proficiencies> (proficiencyType: T, proficiency: Proficiencies[T]) {
    this.proficiencies[proficiencyType].push(...proficiency);
  }
}

If I add 'as never[]' it works, but I wonder if there's any other way to make it transpile.

this.proficiencies[proficiencyType].push(...proficiency as never[]);

EDIT
The answers you gave me worked perfectly but I found out that the error was because of the 'ProfAndBonus' interface that I don't show to simplify.


Solution

  • The fundamental issue is that you haven't made it "explicit enough" that every property of Proficiencies is an array of some type, where that type varies with the key. In addProficiency, this.proficiencies[proficiencyType] has type Proficiencies[T]. When Typescript looks for a push method on this object, it needs to simplify it to something it can do method lookup on. Right now, it can't think of anything better to do except union all the types of the fields of Proficiencies and say it has a string[] | number[] | boolean[]. Then Typescript knows this object must have a push method, but it isn't sure if that push takes ...items: string[] or number[] or boolean[], so plays it safe and says it takes ...items: never[] (which means it isn't useful).

    You need to give Typescript a way to say that Proficiencies[T] is always Something<T>[]. Then, without needing to simplify Something<T>, Typescript can say the push method takes ...items: Something<T>[]. Then, since this is now depending on T, it is possible to call push with a parameter of the right type.

    You can do this by adding a helper type to map between keys and the element types of the arrays.

    interface ProficiencyTypes {
        armor: string
        weapons: string
        tools: string
        savingThrows: number
        skills: boolean
    }
    type MapArray<T> = {
        [Key in keyof T]: T[Key][]
    }
    type Proficiencies = MapArray<ProficiencyTypes> // teach typescript "every key of Proficiencies is a key of ProficiencyTypes but with [] added to its type"
    class Character {
      public readonly proficiencies: Proficiencies = {
        armor: [],
        weapons: [],
        tools: [],
        savingThrows: [],
        skills: []
      }
      public addProficiency<T extends keyof Proficiencies>(proficiencyType: T, proficiency: Proficiencies[T]) {
        this.proficiencies[proficiencyType].push(...proficiency);
      }
    }
    

    Note that now you can't add non-array properties to Proficiencies. That is exactly the freedom that Typescript wants/needs you to give up in order to ensure a function like addProficiency makes sense. In the case of ProfWithBonus, where there is an extra property, you need to first upcast this.proficiencies to just the mapped type (i.e. assure Typescript you won't try to operate on the non-array properties).

    interface ProfAndBonus extends Proficiencies {
      bonus: number
    }
    class Character {
      public readonly proficiencies: ProfAndBonus  = {
        bonus: 2,
        armor: [],
        weapons: [],
        tools: [],
        savingThrows: [],
        skills: []
      }
      public addProficiency<T extends keyof Proficiencies>(proficiencyType: T, proficiency: Proficiencies[T]) {
        let proficiencies: Proficiencies = this.proficiencies
        proficiencies[proficiencyType].push(...proficiency);
      }
    }