Maszynopis: podpisy indeksowe w typie mapowanym
Jak mogę wziąć typ { 'k': number, [s: string]: any }
i streszczenie na 'k'
i number
? Chciałbym mieć taki alias T
typu, 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 funkcjif
działa. - Użycie
[s: string]: any
indeksowanej części wtype
-alias działa - Używanie
k extends string
intype
-alias również działa - Kiedy łączę the
k extends string
z[s: string]: any
in the sametype
-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
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 string
typem, a nie string | number
. Faktycznie zezwolenie number
na dostęp do string
indeksu 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 symbol
jako 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
Oto sposób rozumowania na temat operatora przecięcia. Może to pomaga:
type Intersection = { a: string } & { b: number }
Możesz czytać Intersection
jako „obiekt, który ma właściwość a
typu string
i właściwość b
typu 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 HasKeyValue
rzeczywiście jest taki sam, jak typ, który próbujesz zdefiniować.
Co do tego, dlaczego T_key_and_index
nie działa, to dlatego, że pierwsza część, [a in k]: V
definiuje 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ą& .