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 :
-
keyof T
est une liste exhaustive des clés de T
- Un type avec des propriétés supplémentaires est toujours un sous-type de son type de base.
- Il est légal d'aliaser une valeur de sous-type par une référence de super-types.
-
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);
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é