88 votes

Pourquoi Object.keys ne renvoie-t-il pas un type keyof en TypeScript ?

Le titre dit tout - pourquoi ne pas Object.keys(x) en TypeScript renvoient le type Array<keyof typeof x> ? C'est ce que Object.keys le fait, il semble donc que ce soit un oubli évident de la part des auteurs du fichier de définition TypeScript de ne pas faire en sorte que le type de retour soit simplement keyof T .

Dois-je enregistrer un bogue sur leur repo GitHub, ou simplement envoyer un PR pour le corriger pour eux ?

1 votes

J'ai ouvert et fermé un PR aujourd'hui lié à ce sujet. Mon PR portait uniquement sur le cas où les clés proviennent d'une énumération de chaînes de caractères. Dans ce cas précis, il ne semble pas que l'héritage soit possible. Je dois vérifier avant de la rouvrir. github.com/Microsoft/TypeScript/pull/30228

0 votes

FTR : Ce ^ PR n'a jamais été fusionné

105voto

Ryan Cavanaugh Points 17393

Le type de retour actuel ( string[] ) est intentionnel. Pourquoi ?

Considérez un type comme celui-ci :

interface Point {
    x: number;
    y: number;
}

Vous écrivez un code comme celui-ci :

function fn(k: keyof Point) {
    if (k === "x") {
        console.log("X axis");
    } else if (k === "y") {
        console.log("Y axis");
    } else {
        throw new Error("This is impossible");
    }
}

Posons une question :

Dans un programme bien typé, un appel légal à fn a frappé le cas d'erreur ?

Le site souhaité La réponse est, bien entendu, "non". Mais qu'est-ce que cela a à voir avec Object.keys ?

Maintenant, considérez ceci autre code :

interface NamedPoint extends Point {
    name: string;
}

const origin: NamedPoint = { name: "origin", x: 0, y: 0 };

Notez que selon le système de types de TypeScript, tous les fichiers NamedPoint sont valables Point s.

Maintenant, écrivons un peu plus de code :

function doSomething(pt: Point) {
    for (const k of Object.keys(pt)) {
        // A valid call iff Object.keys(pt) returns (keyof Point)[]
        fn(k);
    }
}
// Throws an exception
doSomething(origin);

Notre programme bien typé vient de lancer une exception !

Quelque chose a mal tourné ici ! En retournant keyof T de Object.keys nous avons violé l'hypothèse selon laquelle keyof T forme une liste exhaustive, car le fait d'avoir une référence à un objet ne signifie pas que le type de la référence n'est pas un supertype du type de la valeur .

Fondamentalement, (au moins) une des quatre choses suivantes ne peut pas être vraie :

  1. keyof T est une liste exhaustive des clés de T
  2. Un type avec des propriétés supplémentaires est toujours un sous-type de son type de base.
  3. Il est légal d'aliaser une valeur de sous-type par une référence de super-types.
  4. Object.keys renvoie à keyof T

Balancer le point 1 fait keyof presque inutile, car elle implique que keyof Point pourrait être une valeur qui n'est pas "x" ou "y" .

L'abandon du point 2 détruit complètement le système de types de TypeScript. Ce n'est pas une option.

L'abandon du point 3 détruit aussi complètement le système de types de TypeScript.

Le fait de rejeter le point 4 est une bonne chose et vous amène, en tant que programmeur, à vous demander si l'objet que vous traitez n'est pas un alias pour un sous-type de l'objet que vous pensez avoir.

La "fonctionnalité manquante" pour rendre cette légales mais non contradictoires est Types exacts ce qui vous permettrait de déclarer une nouvelle genre de type qui n'était pas soumis au point n°2. Si cette fonctionnalité existait, il serait vraisemblablement possible de faire de la Object.keys retourner keyof T seulement pour T qui ont été déclarés comme exact .


Addendum : Des génériques, c'est sûr, mais ?

Les commentateurs ont laissé entendre que Object.keys pourrait retourner en toute sécurité keyof T si l'argument était une valeur générique. C'est toujours faux. Considérez :

class Holder<T> {
    value: T;
    constructor(arg: T) {
        this.value = arg;
    }

    getKeys(): (keyof T)[] {
        // Proposed: This should be OK
        return Object.keys(this.value);
    }
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// Value 'name' inhabits variable of type 'x' | 'y'
const v: "x" | "y" = (h.getKeys())[0];

ou cet exemple, qui ne nécessite même pas d'arguments de type explicites :

function getKey<T>(x: T, y: T): keyof T {
    // Proposed: This should be OK
    return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// Value "name" inhabits variable with type "x" | "y"
const s: "x" | "y" = getKey(obj1, obj2);

11 votes

Toutefois, il est fréquent que le point 3. soit exclu, lorsque par exemple T est déduite et sa précision est garantie : const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) } . J'ai beaucoup d'endroits comme cela dans mon code, où je veux vraiment Object.keys() pour retourner (keyof T)[].

3 votes

Comme arthem le souligne également, la confusion vient du fait que 9 fois sur 10, vous finirez par utiliser d'une manière ou d'une autre une assertion de type pour keyof T de faire quoi que ce soit d'utile avec le résultat de keys . Vous pourriez dire qu'il est préférable d'être explicite à ce sujet afin d'être plus conscient du risque que vous prenez, mais probablement 9/10 devs ajouteront simplement l'assertion de type et ne seront pas conscients des problèmes que vous soulignez .

0 votes

Question/commentaire GitHub pertinent : github.com/Microsoft/TypeScript/pull/

5voto

Adam Pietrasiak Points 728

Je vais essayer d'expliquer pourquoi les clés d'objets ne peuvent pas retourner keyof T tout en étant sûres, à l'aide d'un exemple simple.

// we declare base interface
interface Point {
  x: number;
  y: number;
}

// we declare some util function that expects point and iterates over keys
function getPointVelocity(point: Point): number {
  let velocity = 0;
  Object.keys(point).every(key => {
    // it seems Object.keys(point) will be ['x', 'y'], but it's not guaranteed to be true! (see below)
    // let's assume key is keyof Point
    velocity+= point[key];
  });

  return velocity;
}

// we create supertype of point
interface NamedPoint extends Point {
  name: string;
}

function someProcessing() {
  const namedPoint: NamedPoint = {
    x: 5,
    y: 3,
    name: 'mypoint'
  }

  // ts is not complaining as namedpoint is supertype of point
  // this util function is using object.keys which will return (['x', 'y', 'name']) under the hood
  const velocity = getPointVelocity(namedPoint);
  // !!! velocity will be string '8mypoint' (5+3+'mypoint') while TS thinks it's a number
}

0 votes

Je pense que cet exemple est GREAT dans la mesure où il montre à quel point cette décision de conception de TS est ridicule. Qui pourrait écrire getPointVelocity de cette façon ? Tu peux juste faire point.x + point.y c'est une ligne. J'aimerais voir un meilleur exemple

0 votes

Mon exemple n'est peut-être pas le meilleur, mais je pense qu'il illustre bien le propos. Très souvent, les gens utilisent Object.keys pour, par exemple, enregistrer du contenu dans la base de données et effectuer une sorte de mappage et de sérialisation. Dans ce cas, on peut se retrouver avec plus de données que prévu.

4voto

rattray Points 675

Si vous êtes certain que l'objet sur lequel vous travaillez ne contient pas de propriétés supplémentaires, vous pouvez procéder comme suit :

const obj = {a: 1, b: 2}
const objKeys = Object.keys(obj) as Array<keyof typeof obj>
// objKeys has type ("a" | "b")[]

Vous pouvez extraire cela vers une fonction si vous le souhaitez :

const getKeys = <T>(obj: T) => Object.keys(obj) as Array<keyof T>

const obj = {a: 1, b: 2}
const objKeys = getKeys(obj)
// objKeys has type ("a" | "b")[]

En bonus, voici Object.entries tiré de une question GitHub avec le contexte sur la raison pour laquelle ce n'est pas le défaut :

type Entries<T> = {
  [K in keyof T]: [K, T[K]]
}[keyof T][]

function entries<T>(obj: T): Entries<T> {
  return Object.entries(obj) as any;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X