typescript - Object.values() härleder inte typ från en union eller Records


När det används med TypeScript misslyckas Object.values() med att härleda rätt typ från en union av Records:

type A = Record<string, number>;
type B = Record<string, boolean>;

function func(value: A | B) {
    const properties = Object.values(value); // any[]
}

TS lekplats

Jag förväntar mig att properties är av typen number[] | boolean[], men den faktiska typen är any[].

Finns det ett sätt att härleda rätt typ från Object.values()?

Svar #1

När du kallar Object.values(v) försöker kompilatorn matcha v med {[k: string]: T} för ett generiskt typargument T som den härleder (enligt denna anropssignatur).

Så om v är av en typ som {[k: string]: Z}, kommer kompilatorn att härleda T som Z, och allt kommer att lyckas. Men om v:s typ är en union av poster som {[k: string]: X} | {[k: string]: Y}, är situationen annorlunda. Man kan hoppas att T skulle kunna härledas som unionen X | Y. Men härledningsalgoritmen fungerar inte så. Typen X | Y förekommer inte direkt någonstans i inmatningstypen, och kompilatorn kommer inte att syntetisera unionstyper från flera härledningskandidater. Istället väljer kompilatorn en kandidat ... låt oss säga, X ... och ger sedan ett fel om Record<string, X> | Record<string, Y> inte är tilldelningsbar till Record<string, X>.

Denna vägran att syntetisera uniontyper är avsiktlig; Ofta föredrar folk att se ett fel än att kompilatorn börjar skapa unioner för att få saker att lyckas, särskilt eftersom det i många fall skulle göra nästan varje samtal framgångsrikt, även de som gör galna saker som compare(3, "oops"), förutsatt att compare är av en typ som <T>(a: T, b: T)=> tal. Folk vill se att det misslyckas istället för att T ska härledas som tal | sträng.

I ditt fall vill du dock att den unionen ska härledas. Det finns en öppen funktionsförfrågan på microsoft/TypeScript#44312 för att ha ett sätt att säga att sådan härledning bör ske. Men det är ännu inte en del av språket, och även om det vore det, kanske inte Object.values()-anropssignaturen ändras för att använda det.

Så vi kan inte förlita oss på kompilatorn för att direkt härleda den unionstyp du vill ha.


Istället kan vi specificera unionstypen när vi anropar Object.values(). Det skulle se ut som Object.values<X | Y>(v). I ditt exempel kan du då bara skriva Object.values<number | boolean>(value), och du är klar.

Men du vill inte hårdkoda den typen av argument på det sättet. Du vill att det ska bero på value så att om dess typ ändras till någon annan förening av poster, skulle saker fortsätta att fungera. Så det betyder: givet ett värde value av en typ som {[k: string]: X} | {[k: string]: Y}, hur kan vi beräkna typen X | Y?

Här kan vi använda typeof-typen frågeoperator på value för att få typen av value, och sedan kan vi indexera i den typen med string för att få typen av egenskaperna hos value vid en string-nyckel.

Det vill säga:

Object.values<typeof value[string]>(value);/okej

Eller, för att se det allmänna exemplet:

function foo<X, Y>() {
    type A = Record<string, X>;
    type B = Record<string, Y>;
    function func(value: A | B) {
        return Object.values<typeof value[string]>(value);        
    }
    // func(value: Record<string, X> | Record<string, Y>): (X | Y)[]
}

Inuti foo()func() ett värde av typen A | B, och returnerar ett värde av typen (X | Y)[], utan att vi behöver hårdkoda typen X | Y själva. Istället utvärderas typeof value[string] till X | Y och vi använder det.

Playground-länk till kod

Svar #2

Så här kan du definiera gränssnittet för en patchead objektkonstruktion som heter ObjectConstructorAlt.

declare interface ObjectConstructorAlt {
    values<T>(o: T):
    T extends any ?
        T extends Record<string|number, infer V> ? V[] 
            : T extends (infer V)[] ? V[]
            : any
        : never       
    ;
}
type A = Record<string, number>;
type B = Record<string, boolean>;
declare const a:A;
declare const b:B;
declare const ab:A|B;

Object.values(a); // number[]
Object.values(b); // boolean[]
Object.values(ab); // any[]

// actual implementation
const ObjectAlt: ObjectConstructorAlt = {
    values<T>(x: T){ 
        return Object.values(x as object) as any; 
    }
}
ObjectAlt.values(a); // number[]
ObjectAlt.values(b); // boolean[]
ObjectAlt.values(ab); // number[] | boolean[]

Typescript

Om du kunde övertala TypeScript-underhållarna att byta ut den nuvarande ObjectConstructor

interface ObjectConstructor {
    values<T>(o: { [s: string]: T } | ArrayLike<T>): T[];
    values(o: {}): any[];
    ...
}

för något som ObjectConstructorAlt skulle en patch på din sida inte vara nödvändig. Det är dock osannolikt, eftersom det kan öka kompileringstiden och orsaka att andra saker går sönder.

uppdatering - FWIW skickade jag in detta som ett problem till TypeScript och det nuvarande beteendet hos Object.values/entries har fastställts som inte ett fel. Det finns mycket historia bakom det beslutet som är värd att läsa.

Relaterade artiklar