Typoskript: Indexsignaturen im zugeordneten Typ

Nov 23 2020

Wie kann ich den Typ { 'k': number, [s: string]: any }und die Zusammenfassung übernehmen 'k'und number? Ich hätte gerne einen Typ-Alias T, T<'k', number>der den genannten Typ angibt.


Betrachten Sie das folgende Beispiel:

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 };// ?
  • Die { 'k': number, [s: string]: any}direkte Verwendung als Typ des Parameters der Funktion ffunktioniert.
  • Die Verwendung des [s: string]: anyindizierten Teils in type-alias funktioniert
  • Die Verwendung von k extends stringin type-alias funktioniert ebenfalls
  • Wenn ich das k extends stringmit dem [s: string]: anygleichen typeAlias ​​kombiniere, erhalte ich einen Analysefehler (nicht einmal einen semantischen Fehler, es scheint nicht einmal eine gültige Syntax zu sein).

Das hier scheint zu funktionieren:

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }

Aber hier kann ich nicht ganz verstehen, warum es sich nicht über zusätzliche Eigenschaften beschwert (der Typ auf der rechten Seite &sollte keine Objekte mit zusätzlichen Eigenschaften zulassen).


EDIT :

In den Antworten wurde mehrfach erwähnt, dass dies der &Schnittpunktoperator ist, der sich ähnlich wie der satztheoretische Schnittpunkt verhalten soll. Dies ist jedoch nicht der Fall, wenn es um die Behandlung der zusätzlichen Eigenschaften geht, wie das folgende Beispiel zeigt:

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!

In diesem Beispiel scheint es, als ob das {a: 42, b: 58}weder vom Typ {a: number}noch vom Typ ist {b: number}, aber es endet irgendwie in der Kreuzung {a: number} & {b: number}. So funktioniert die satztheoretische Schnittmenge nicht.

Das ist genau der Grund, warum mein eigener &Vorschlag für mich so verdächtig aussah. Ich würde mich freuen, wenn jemand erläutern könnte, wie durch "Überschneiden" eines zugeordneten Typs { [s: string]: any }der Typ "größer" werden kann, anstatt ihn kleiner zu machen.


Ich habe die Fragen gesehen

  • Indexsignatur für einen zugeordneten Typ in Typescript
  • Wie füge ich eine Indexsignatur für einen zugeordneten Typ hinzu?

aber diese schienen nicht direkt verwandt zu sein, obwohl sie einen ähnlichen Namen hatten.

Antworten

1 arturgrzesiak Dec 01 2020 at 18:58

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }ist der richtige Weg, um den Typ zu definieren, nach dem Sie suchen. Aber eine Sache zu wissen ist ( paraphrasieren veralteter Flagge: keyofStringsOnly ):

Der Operator keyof type gibt die Zeichenfolge | zurück Zahl anstelle von Zeichenfolge, wenn sie auf einen Typ mit einer Zeichenfolgenindexsignatur angewendet wird.

Ich kenne keine Methode, um den Index auf den stringTyp zu beschränken und nicht string | number. Tatsächlich scheint es vernünftig zu sein, numberauf den stringIndex zuzugreifen , da dies der Funktionsweise von Javascript entspricht (man kann immer eine Zahl stringifizieren). Andererseits können Sie nicht sicher auf einen Zahlenindex mit einem Zeichenfolgenwert zugreifen.


Der &Typoperator arbeitet ähnlich, um theoretische Schnittpunkte festzulegen - er beschränkt immer die Menge möglicher Werte (oder lässt sie unverändert, erweitert sie jedoch nie). In Ihrem Fall schließt der Typ alle nicht stringähnlichen Schlüssel als Index aus. Um genau zu sein, schließen Sie unique symbolals Index aus.

Ich denke, Ihre Verwirrung kann von der Art und Weise herrühren, wie Typescript Funktionsparameter behandelt. Das Aufrufen einer Funktion mit explizit definierten Parametern verhält sich anders als das Übergeben von Parametern als Variablen. In beiden Fällen stellt Typescript sicher, dass alle Parameter die richtige Struktur / Form haben, im letzteren Fall sind jedoch keine zusätzlichen Requisiten zulässig.


Code zur Veranschaulichung der Konzepte:

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

BEARBEITEN

Objektliterale - Das explizite Erstellen von Objekten mit einer bestimmten Form const p: { a: number} = { a: 42 }wird von Typescript auf besondere Weise behandelt. Im Gegensatz zur regulären strukturellen Inferenz muss der Typ genau angepasst werden. Und um ehrlich zu sein, ist es sinnvoll, da diese zusätzlichen Eigenschaften - ohne zusätzlichen, möglicherweise unsicheren Guss - ohnehin nicht zugänglich sind.

[...] TypeScript vertritt jedoch die Auffassung, dass dieser Code wahrscheinlich einen Fehler enthält. Objektliterale werden speziell behandelt und auf ihre Eigenschaften überprüft, wenn sie anderen Variablen zugewiesen oder als Argumente übergeben werden. Wenn ein Objektliteral Eigenschaften hat, die der „Zieltyp“ nicht hat, wird eine Fehlermeldung angezeigt. [...] Eine letzte Möglichkeit, diese Überprüfungen zu umgehen, die etwas überraschend sein kann, besteht darin, das Objekt einer anderen Variablen zuzuweisen.

TS Handbuch

Die andere Möglichkeit, diesen Fehler zu umgehen, besteht darin, ... ihn mit zu überschneiden { [prop: string]: any }.

Mehr 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
1 artcorpse Nov 30 2020 at 18:05

Hier ist eine Argumentationsweise für den Kreuzungsoperator. Vielleicht hilft es:

type Intersection = { a: string } & { b: number }

Sie können Intersectionals "ein Objekt mit einer Eigenschaft avom Typ string und einer Eigenschaft bvom Typ number" lesen . Das beschreibt auch diesen einfachen Typ:

type Simple = { a: string; b: number }

Und die beiden Typen sind kompatibel. Sie können für fast alle Zwecke eine durch die andere ersetzen.

Ich hoffe, dies erklärt, warum HasKeyValuetatsächlich derselbe Typ ist, den Sie definieren wollten.

Warum T_key_and_indexnicht funktioniert, liegt daran, dass der erste Teil [a in k]: Veinen zugeordneten Typ definiert und Sie in der Definition eines zugeordneten Typs keine zusätzlichen Eigenschaften haben können. Wenn Sie einem zugeordneten Typ zusätzliche Eigenschaften hinzufügen müssen, können Sie einen Typschnittpunkt mit& erstellen .