typescript - Hur kan jag sätta en generisk begränsning för att säkerställa att en gemensam nyckel mellan två typer också har samma typen på värdet?
Jag skapar en funktion för att kopiera fält från ett objekt till ett annat och jag skulle vilja garantera:
- Jag kan bara ge en nyckel som är gemensam mellan båda objekten
- den angivna nyckeln motsvarar samma värdetyp i båda typerna
Hur kan jag implementera den andra begränsningen? Här är vad jag har hittills.
function copyField<T1, T2>(input: T1, output: T2, key: keyof T1 & keyof T2) {
const keyValue = input[key]
output[key] = keyValue // How can I ensure that the key value is the same for both?
// ^ Error: Type 'T1[keyof T1 & keyof T2]' is not assignable to type 'T2[keyof T1 & keyof T2]'.
// Type 'T1' is not assignable to type 'T2'. 'T2' could be instantiated with an
// arbitrary type which could be unrelated to 'T1'.
}
type A = { field: string }
type B = { field: string }
copyField<A, B>({field: 'hello'}, {field: 'world'}, 'field')
Live-exempel på typescriptlang.org
Uppdatera
Ett svar nedan tog mig närmare det jag vill ha men garanterar fortfarande inte att värdet för den angivna nyckeln är exakt detsamma. Detta visar hur felmatchade typer fortfarande kan kompileras eftersom begränsningen bara garanterar att typen finns någonstans på båda objekten, inte nödvändigtvis för den angivna nyckeln.
function copyField<K extends string, V>(input: {[k in K]: V}, output: {[k in K]: V}, key: K) {
const keyValue = input[key]
output[key] = keyValue
}
type A = { s: string, mismatch: string }
type B = { s: string, mismatch: number }
// ^--- these don't have the same type
const a: A = {s: 'hello', mismatch: 'hello'}
const b: B = {s: 'world', mismatch: 123}
// This compiles, but I want it to only accept 's'
copyField(a, b, 'mismatch')
Svar #1
Här är en lösning:
type CommonKeys<S, T> = {
[K in keyof S & keyof T]:
[S[K], T[K]] extends [T[K], S[K]] ? K : never
}[keyof S & keyof T]
Det fungerar genom att först konstruera en mappad objekttyp som {s: 's', mismatch: never}, och sedan mappa det till 's' | never vilket naturligtvis är 's'.
Varje nyckel K inkluderas om [S[K], T[K]] extends [T[K], S[K]], vilket kontrollerar att S[K] och T[K] är subtyper av varandra, dvs samma typ. Om detta är för strikt för vad du vill, kan du ändra detta till S[K] extends T[K] för att bara kontrollera att inmatningsvärdets typ kan tilldelas utmatningsvärdets typ.
Lekplatslänk
Svar #2
Om det spelar någon roll, här är hur jag skulle tänka mig att göra det:
function copyField<O, K extends keyof O, I extends Record<K, O[K]>>(
input: I,
output: O,
key: K
) {
const i: Record<K, O[K]> = input;
output[key] = i[key];
}
Idén är att du tillåter typen O av output vara vad du vill, och tillåter typen K av key att vara vilken nyckel som helst i O. Sedan måste typen I av input begränsas till Record<K, O[K]> (se dokumentationen för Record), vilket innebär att input måste kunna tilldelas något med en egenskap vid nyckel K vars värde måste kunna tilldelas O[K], typen av samma egenskap i O.
Observera att jag inte kräver att I[K] och O[K] ska vara identiska... det borde vara tillräckligt att kräva att I[K] extends O[K] för att tillåta bra kopior och förhindra dåliga.
Observera också: för att tilldelningen ska accepteras av kompilatorn inne i funktionsimplementeringen behövde jag utvidga input från I till dess begränsning Record<K, O[K]>.
Detta ger följande önskvärda beteende:
copyField({ good: 0, bad: "", whatever: true }, { good: 4, bad: 1 }, "good"); // okay
copyField({ good: 0, bad: "", whatever: true }, { good: 4, bad: 1 }, "bad"); // error!
// ----------------> ~~~
// Type 'string' is not assignable to type 'number'
Observera att felet här inte verkar på värdet "bad" för key, utan på parametern input. I det här fallet varnar det specifikt för att egenskapen bad är en string, men du kan inte skriva en string i en number-egenskap.
På samma sätt kommer ovanstående definition att tillåta dig att skriva från en snäv typ till en bredare typ, men inte tvärtom:
copyField({ x: 123 }, { x: Math.random() < 0.5 ? 456 : "789" }, "x"); // okay
copyField({ x: Math.random() < 0.5 ? 456 : "789" }, { x: 123 }, "x"); // error!
// -------> ~
// Type 'string | number' is not assignable to type 'number'
Den första raden fungerar eftersom det är säkert att kopiera number-värdet från input till det bredare number | string-värdet från output. Men den andra raden klagar eftersom du inte säkert kan kopiera ett number | string-värde från input till det smalare number-värdet från output. Och varningen förklarar det.
Lekplatslänk till kod
