Maszynopis: podpisy indeksowe w typie mapowanym

Nov 23 2020

Jak mogę wziąć typ { 'k': number, [s: string]: any }i streszczenie na 'k'i number? Chciałbym mieć taki alias Ttypu, który T<'k', number>określa ten typ.


Rozważmy następujący przykład:

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 };// ?
  • Używanie { 'k': number, [s: string]: any}bezpośrednio jako typu parametru funkcji fdziała.
  • Użycie [s: string]: anyindeksowanej części w type-alias działa
  • Używanie k extends stringin type-alias również działa
  • Kiedy łączę the k extends stringz [s: string]: anyin the same type-alias, otrzymuję błąd analizy (nawet nie jest to błąd semantyczny, nawet nie wydaje się być poprawną składnią).

To tutaj wydaje się działać:

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

ale tutaj nie do końca rozumiem, dlaczego nie narzeka na dodatkowe właściwości (typ po prawej stronie &nie powinien dopuszczać obiektów z dodatkowymi właściwościami).


EDYCJA :

W odpowiedziach kilkakrotnie wspomniano, że &jest operatorem przecięcia, który ma zachowywać się podobnie do przecięcia teoretycznego. Jednak nie jest tak, jeśli chodzi o traktowanie dodatkowych właściwości, jak pokazuje poniższy przykład:

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!

W tym przykładzie wydaje się, że {a: 42, b: 58}nie jest ani typu {a: number}, ani typu {b: number}, ale w jakiś sposób kończy się na przecięciu {a: number} & {b: number}. Nie tak działa przecięcie teoretyczne.

Właśnie dlatego moja &propozycja wydała mi się tak podejrzana. Byłbym wdzięczny, gdyby ktoś mógł wyjaśnić, w jaki sposób „przecięcie” mapowanego typu z { [s: string]: any }może sprawić, że typ będzie „większy”, zamiast zmniejszać go.


Widziałem pytania

  • Podpis indeksu dla mapowanego typu w Typescript
  • Jak dodać podpis indeksu dla mapowanego typu

ale te nie wydawały się bezpośrednio powiązane, chociaż miały podobną nazwę.

Odpowiedzi

1 arturgrzesiak Dec 01 2020 at 18:58

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }to właściwy sposób na zdefiniowanie typu, którego szukasz. Ale jedna rzecz, którą należy wiedzieć, to ( parafrazowanie flagi przestarzałej: keyofStringsOnly ):

Operator typu keyof zwraca string | liczba zamiast ciągu w przypadku zastosowania do typu z podpisem indeksu ciągu.

Nie znam metody ograniczania indeksu, aby był tylko stringtypem, a nie string | number. Faktycznie zezwolenie numberna dostęp do stringindeksu wydaje się rozsądną rzeczą, ponieważ jest zgodne ze sposobem działania Javascript (zawsze można wpisać liczbę). Z drugiej strony nie można bezpiecznie uzyskać dostępu do indeksu liczbowego z wartością ciągu.


&Operator typu działa podobnie do teoretyczną skrzyżowanie - to zawsze ogranicza zbiór możliwych wartości (lub pozostawia je niezmienione, ale nie rozciąga). W twoim przypadku typ wyklucza klucze inne niż ciągi jako indeks. Aby być precyzyjnym, wykluczasz unique symboljako indeks.

Myślę, że twoje zamieszanie może wynikać ze sposobu, w jaki Typescript traktuje parametry funkcji. Wywołanie funkcji z jawnie zdefiniowanymi parametrami zachowuje się inaczej niż przekazywanie parametrów jako zmiennych. W obu przypadkach Typescript zapewnia, że ​​wszystkie parametry mają poprawną strukturę / kształt, ale w tym drugim przypadku dodatkowo nie pozwala na dodatkowe rekwizyty.


Kod ilustrujący pojęcia:

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

EDYTOWAĆ

Literały obiektowe - jawne tworzenie obiektu o jakimś kształcie const p: { a: number} = { a: 42 }jest traktowane przez Typescript w specjalny sposób. W przeciwieństwie do zwykłego wnioskowania strukturalnego, typ musi być dokładnie dopasowany. Szczerze mówiąc, ma to sens, ponieważ te dodatkowe właściwości - bez dodatkowego, potencjalnie niebezpiecznego odlewu - są i tak niedostępne.

[...] Jednak TypeScript przyjmuje stanowisko, że prawdopodobnie jest błąd w tym kodzie. Literały obiektów są traktowane w specjalny sposób i przechodzą nadmierne sprawdzanie właściwości podczas przypisywania ich do innych zmiennych lub przekazywania ich jako argumentów. Jeśli literał obiektu ma właściwości, których nie ma „typ docelowy”, pojawi się błąd. [...] Ostatnim sposobem obejścia tych kontroli, co może być nieco zaskakujące, jest przypisanie obiektu do innej zmiennej.

Podręcznik TS

Inną opcją obejścia tego błędu jest ... przecięcie go z { [prop: string]: any }.

Więcej kodu:

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

Oto sposób rozumowania na temat operatora przecięcia. Może to pomaga:

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

Możesz czytać Intersectionjako „obiekt, który ma właściwość atypu string i właściwość btypu number”. To też opisuje ten prosty typ:

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

Te dwa typy są kompatybilne. Możesz zastąpić jeden drugim w prawie wszystkich celach.

Mam nadzieję, że to wyjaśnia, dlaczego HasKeyValuerzeczywiście jest taki sam, jak typ, który próbujesz zdefiniować.

Co do tego, dlaczego T_key_and_indexnie działa, to dlatego, że pierwsza część, [a in k]: Vdefiniuje mapowany typ , aw definicji mapowanego typu nie można mieć dodatkowych właściwości. Jeśli chcesz dodać dodatkowe właściwości do mapowanego typu, możesz utworzyć przecięcie typów za pomocą& .