0

This is my code:

interface Siswa {
  name: string,
  points: { a: number, b: number, c: number, d?: number, e?: number }
}

function find_average_points(data: Array<Siswa>): Array<{name: string, average: number}> {
  let returned_array: Array<{name: string, average: number}> = [];
  data.forEach((item: Siswa) => {
    let sum: number = 0;
    let keys = Object.keys(item.points);
    keys.forEach((key: any) => sum+=item.points[key]);
    returned_array.push({name: item.name, average: (sum/keys.length)});
  });
  return returned_array;
}

When I tried this function in JavaScript, it ran correctly and the result is what I want, but in TypeScript, I got an error in item.points[key]. It says:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ a: number; b: number; c: number; d?: number | undefined; e?: number | undefined; }'.
  No index signature with a parameter of type 'string' was found on type '{ a: number; b: number; c: number; d?: number | undefined; e?: number | undefined; }'

I don't know what it means.

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
riki yudha
  • 35
  • 1
  • 7
  • 1
    It's an error because [objects may have more keys than are declared in their interfaces](https://tsplay.dev/wj40Mm), so unless you want to add possibly random things to your `sum`, you should probably use a hardcoded array of keys and filter them [like this](https://tsplay.dev/wenzYN). If this meets your needs I'm happy to write up an answer explaining this (tomorrow since it's my bedtime now); if not, please let me know what use case is unsatisfied. – jcalz Nov 26 '21 at 05:03

2 Answers2

3

Using Object.keys() returns the type string[] (as explained in this comment). Instead, you can create arrays for the known keys in the points object, use them to build your types, and then loop through those keys when summing the values:

TS Playground link

const definitePoints = ['a', 'b', 'c'] as const;
const maybePoints = ['d', 'e'] as const;

type Points = (
  Record<typeof definitePoints[number], number>
  & Partial<Record<typeof maybePoints[number], number>>
);

interface Siswa {
  name: string,
  points: Points;
}

interface SiswaAverage {
  name: string;
  average: number;
}

function find_average_points (data: Array<Siswa>): Array<SiswaAverage> {
  const result: Array<SiswaAverage> = [];

  for (const {name, points} of data) {
    let sum = 0;
    let count = 0;

    for (const point of [...definitePoints, ...maybePoints]) {
      const value = points[point];
      if (typeof value !== 'number') continue;
      sum += value;
      count += 1;
    }

    result.push({name, average: sum / count});
  }

  return result;
}
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
1

You can use a combination of keyof and null check to solve the problem.

    interface Points { a: number, b: number, c: number, d?: number, e?: number } // only did to use keyof

interface Siswa {
  name: string,
  points: Points
}

function find_average_points(data: Array<Siswa>): Array<{name: string, average: number}> {
  let returned_array: Array<{name: string, average: number}> = [];
  data.forEach((item: Siswa) => {
    let sum: number = 0;
    let keys = Object.keys(item.points) as (keyof Points)[]; // use this if you know you wont add extra non number properties in Point 

    keys.forEach((key) => sum+=item.points[key] ?? 0); // for d and e with are optional
    returned_array.push({name: item.name, average: (sum/keys.length)});
  });
  return returned_array;
}
let obj: Siswa = { name: 'Paris', points: { a: 2 , b:2, c: 4, d: 4 }} // can not add random stuff type safe

console.log(find_average_points([obj]))
vaira
  • 2,191
  • 1
  • 10
  • 15
  • Since you used the `whatsit` code from my comment in this answer, can you explain what your method to deal with it is? `Object.keys()` returns `string[]` [on purpose](//stackoverflow.com/q/55012174/2887218). By writing `Object.keys(item.points) as (keyof Point)[]` you have lied to the compiler about what `Object.keys(item.points)` contains. You could argue that a case like `whatsit` with extra properties is unlikely to happen in reality so you don't need to worry about it in practice, but you probably should say *something* about it. Especially if you include the problem case in your code. – jcalz Nov 26 '21 at 15:49
  • @jcalz to be very very honest, I had not even read your comment before writing this, since this is the most obvious solution, i have not lied to the compiler even once, by stating as '(keyof Point)[]' has set the type of "let keys: (keyof Point)[]" which has always been my intent. – vaira Nov 26 '21 at 16:27
  • So why does your answer have `whatsit` in it, and what is your method to deal with it? The fact that `NaN` comes out is a problem, right? Because `sum += items.points[key] ?? 0` still can end up performing `+=` on various non-numeric values (one could [go crazy](https://tsplay.dev/w8KYAW) with it). You commented `// wrong data type if const whatsit: Siswa`; is that supposed to serve as an explanation or mitigation? If so, can you use words in your answer to explain? As it stands, the comment `// defined what it really is` is not accurate. Help me out here! – jcalz Nov 26 '21 at 16:38
  • Oh now i get what you are talking about, i apologise i might have confused your type script playground code for the the question. I will update it with orignal question code one i am back. – vaira Nov 27 '21 at 00:17
  • Updated with orignal question: @jcalz You have created `whatit` of type `any` that is why you are able to enter incorrect values in 'd' and 'e', which is not a real case, that is why your average was coming as 'NAN' which also not possible if it was just typesafed `whatit:Siswa` – vaira Nov 27 '21 at 01:07
  • @jcalz `Object.keys(item.points) as (keyof Point)[]` is not a lie, its the trueth. – vaira Nov 27 '21 at 01:13
  • No, [look again](https://tsplay.dev/mAvx8W); `whatsit` is absolutely *not* of type `any`, it is of type `{ name: string; points: { a: number; b: number; c: number; z: Date; y: {}; x: boolean; w: () => number; };` (in that example). What you are calling "type safe" is just [excess property checking](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#stricter-object-literal-assignment-checks), which is more of a linter check and not a type restriction. Object types in TypeScript are open and extendible; they are allowed to have more properties than those declared. – jcalz Nov 27 '21 at 01:18
  • Please see [this question](https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript) which is asked and answered by the development lead for the TypeScript team it Microsoft. `Object.keys(x)` returns `string[]`, not `(keyof typeof x)[]`; this is on purpose and intentional for exactly the reason I showed with `whatsit`. When you write `Object.keys(item.points) as (keyof Point)[]` you are quite possibly lying, as you cannot guarantee that `item.points` has *only* known properties. – jcalz Nov 27 '21 at 01:20
  • Yet again, one could make the argument that this will not happen in practice, and that one should not necessarily guard against it, and that the type assertion is therefore likely to be harmless and unlikely to be a lie. But this is an argument you need to make; it is simply not true that objects are restricted to only contain known properties. There is an open feature request at https://github.com/microsoft/TypeScript/issues/12936 asking for such a feature (called "exact types"), but it is not implemented in TypeScript. Objects can have extra properties. – jcalz Nov 27 '21 at 01:22
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/239622/discussion-between-vaira-and-jcalz). – vaira Nov 27 '21 at 01:26