Typographie: signatures d'index dans le type mappé
Comment puis-je reprendre le type { 'k': number, [s: string]: any }
et le résumé 'k'
et number
? Je voudrais avoir un alias de T
type qui T<'k', number>
donne ledit type.
Prenons l'exemple suivant:
function f(x: { 'k': number, [s: string]: any }) {} // ok
type T_no_params = { 'k': number, [s: string]: any }; // ok
type T_key_only<k extends string> = { [a in k]: number }; // ok
type T_value_only<V> = { 'k': V, [s: string]: any}; // ok
type T_key_and_index<k extends string, V> = { [a in k]: V, [s: string]: any };// ?
- Utiliser
{ 'k': number, [s: string]: any}
directement comme type du paramètre de la fonctionf
fonctionne. - L'utilisation de la
[s: string]: any
partie indexée danstype
-alias fonctionne - L'utilisation
k extends string
detype
-alias fonctionne également - En combinant le
k extends string
avec le[s: string]: any
mêmetype
-alias, j'obtiens une erreur d'analyse (même pas une erreur sémantique, cela ne semble même pas être une syntaxe valide).
Cela semble fonctionner ici:
type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }
mais ici, je ne comprends pas très bien pourquoi il ne se plaint pas de propriétés supplémentaires (le type à droite de &
ne devrait pas autoriser les objets avec des propriétés supplémentaires).
MODIFIER :
Il a été mentionné à plusieurs reprises dans les réponses que le &
est l'opérateur d'intersection, qui est censé se comporter de la même manière que l'intersection théorique des ensembles. Ce n'est cependant pas le cas en ce qui concerne le traitement des propriétés supplémentaires, comme le montre l'exemple suivant:
function f(x: {a: number}){};
function g(y: {b: number}){};
function h(z: {a: number} & {b: number}){};
f({a: 42, b: 58}); // does not compile. {a: 42, b: 58} is not of type {a: number}
g({a: 42, b: 58}); // does not compile. {a: 42, b: 58} is not of type {b: number}
h({a: 42, b: 58}); // compiles!
Dans cet exemple, il semble que le {a: 42, b: 58}
n'est ni de type {a: number}
, ni de type {b: number}
, mais il aboutit d'une manière ou d'une autre à l'intersection {a: number} & {b: number}
. Ce n'est pas ainsi que fonctionne l'intersection théorique des ensembles.
C'est exactement la raison pour laquelle ma propre &
proposition me paraissait si suspecte. J'apprécierais que quelqu'un puisse expliquer comment "l'intersection" d'un type mappé avec { [s: string]: any }
pourrait rendre le type "plus grand" au lieu de le rendre plus petit.
J'ai vu les questions
- Signature d'index pour un type mappé dans Typescript
- Comment ajouter une signature d'index pour un type mappé
mais ceux-ci ne semblaient pas directement liés, bien que portant un nom similaire.
Réponses
type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }
est la bonne façon de définir le type que vous recherchez. Mais une chose à savoir est ( paraphrasant l'indicateur obsolète: keyofStringsOnly ):
L'opérateur de type keyof renvoie la chaîne | nombre au lieu de chaîne lorsqu'il est appliqué à un type avec une signature d'index de chaîne.
Je ne connais pas de méthode pour restreindre l'index à être juste le string
type et non string | number
. En fait, autoriser l' number
accès à l' string
index semble une chose raisonnable, car cela correspond au fonctionnement de Javascript (on peut toujours stringifier un nombre). D'un autre côté, vous ne pouvez pas accéder en toute sécurité à un index numérique avec une valeur de chaîne.
L' &
opérateur type fonctionne de la même manière pour définir l'intersection théorique - il restreint toujours l'ensemble des valeurs possibles (ou les laisse inchangées, mais ne les étend jamais). Dans votre cas, le type exclut toutes les clés qui ne ressemblent pas à une chaîne comme index. Pour être précis, vous excluez unique symbol
comme index.
Je pense que votre confusion peut provenir de la façon dont Typescript traite les paramètres de fonction. L'appel d'une fonction avec des paramètres définis explicitement se comporte différemment du passage de paramètres en tant que variables. Dans les deux cas, Typescript s'assure que tous les paramètres sont de structure / forme correcte, mais dans ce dernier cas, il n'autorise pas non plus les accessoires supplémentaires.
Code illustrant les concepts:
type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V };
type WithNumber = HasKeyValue<"n", number>;
const x: WithNumber = {
n: 1
};
type T = keyof typeof x; // string | number
x[0] = 2; // ok - number is a string-like index
const s = Symbol("s");
x[s] = "2"; // error: cannot access via symbol
interface N {
n: number;
}
function fn(p: N) {
return p.n;
}
const p1 = {
n: 1
};
const p2 = {
n: 2,
s: "2"
};
fn(p1); // ok - exact match
fn(p2); // ok - structural matching: { n: number } present; additional props ignored
fn({ n: 0, s: "s" }); // error: additional props not ignore when called explictily
fn({}); // error: n is missing
ÉDITER
Objets littéraux - la création explicite d'objet d'une forme semblable const p: { a: number} = { a: 42 }
est traitée par Typescript d'une manière spéciale. Contrairement à l'inférence structurelle régulière, le type doit correspondre exactement. Et pour être honnête, cela a du sens, car ces propriétés supplémentaires - sans fonte supplémentaire éventuellement dangereuse - sont de toute façon inaccessibles.
[...] Cependant, TypeScript prend la position qu'il y a probablement un bogue dans ce code. Les littéraux d'objet reçoivent un traitement spécial et subissent une vérification de propriété excessive lors de leur affectation à d'autres variables ou de leur transmission en tant qu'arguments. Si un objet littéral a des propriétés que le «type cible» n'a pas, vous obtiendrez une erreur. [...] Une dernière façon de contourner ces vérifications, qui peut être un peu surprenante, est d'assigner l'objet à une autre variable.
Manuel TS
L'autre option pour contourner cette erreur est de ... l'intersecter avec { [prop: string]: any }
.
Plus de code:
function f(x: { a: number }) {}
function g(y: { b: number }) {}
function h(z: { a: number } & { b: number }) {}
f({ a: 42, b: 58 } as { a: number }); // compiles - cast possible, but `b` inaccessible anyway
g({ a: 42 } as { b: number }); // does not compile - incorrect cast; Conversion of type '{ a: number; }' to type '{ b: number; }' may be a mistake
h({ a: 42, b: 58 }); // compiles!
const p = {
a: 42,
b: 58
};
f(p); // compiles - regular structural typing
g(p); // compiles - regular structural typing
h(p); // compiles - regular structural typing
const i: { a: number } = { a: 42, b: 58 }; // error: not exact match
f(i); // compiles
g(i); // error
h(i); // error
Voici une manière de raisonner sur l'opérateur d'intersection. Peut-être que cela aide:
type Intersection = { a: string } & { b: number }
Vous pouvez lire Intersection
comme "un objet qui a une propriété a
de type string
et une propriété b
de type number
". Cela se trouve pour décrire également ce type simple:
type Simple = { a: string; b: number }
Et les deux types sont compatibles. Vous pouvez remplacer l'un par l'autre à presque toutes les fins.
J'espère que cela explique pourquoi HasKeyValue
est en effet le même que le type que vous essayiez de définir.
Quant à savoir pourquoi T_key_and_index
ne fonctionne pas, c'est parce que la première partie [a in k]: V
` ` définit un type mappé , et dans la définition d'un type mappé, vous ne pouvez pas avoir de propriétés supplémentaires. Si vous devez ajouter des propriétés supplémentaires à un type mappé, vous pouvez créer une intersection de type avec& .