Ersätt TypeScript-fel "uttryck av typ X kan inte användas för att indexera typ Y" med undefined
Överväg följande kod:
const myObject = {
foo: 'val1',
bar: 'val2'
} as const
type keys = keyof typeof myObject // 'foo' | 'bar'
const key: string = getKey() // returns some random string key
const result: 'val1' | 'val2' | undefined = myObject[key]
Typescript ger ett fel vid myObject[key] med meddelandet Element har implicit typen 'any' eftersom ett uttryck av typen 'string' inte kan användas som index för typen '{ readonly foo: "val1"; readonly bar: "val2"; }'. Det stämmer, men det gör mig inget - jag har inga problem med att uttrycket utvärderas till undefined om key varken är foo eller bar.
Jag vill att typeof result ska vara 'val1' | 'val2' | undefined, vilket är helt giltigt - inga påståenden behövs.
Finns det något sätt att tvinga TS att göra detta?
Svar #1
TypeScript har inte direkt stöd för så kallade "exakta typer", som begärt i microsoft/TypeScript#12936. Alla objekttyper i TypeScript betraktas som "öppna" eller "utbyggbara", så det är alltid möjligt att ett värde har fler egenskaper än vad som är känt i typen. Ett värde av typen {a: string} är endast känt för att innehålla en egenskap med värdetyp string vid nyckeln "a". Det är inte känt att det saknar en egenskap vid exempelvis nyckeln "b". Därför är typen av {a: string}["b"] mer lik unknown (det kan vara vad som helst) än undefined (det är känt att den inte existerar). Även objektsliteraler som
const o = { foo: 'val1', bar: 'val2' } as const;
/* const o: {
readonly foo: "val1";
readonly bar: "val2";
} */
har sådana tekniskt utbyggbara typer:
const p = { foo: 'val1', bar: 'val2', baz: 'val3' } as const;
const q: typeof o = p; // okay
Det finns inte ens ett sätt att konsekvent uttrycka en sådan typ i typesystemet. Det närmaste man kan komma är att ge typen en indexsignatur för att beskriva egenskaperna vid godtyckliga nycklar. Men man kan inte använda indexsignaturer för att göra undantag för kända nycklar. Det vill säga, följande typ är ogiltig:
interface Nope {
readonly foo: "val1";
readonly bar: "val2";
readonly [rest: string]: undefined;
}
Indexsignaturen här betyder att varje egenskap med en string-nyckel kommer att ha ett undefined-värde. Detta strider mot "foo"- och "bar"-egenskaperna och kompilatorn klagar. Det finns en långvarig öppen funktionförfrågan på microsoft/TypeScript#17867 för att tillåta en "rest"-indexsignatur som gör undantag för kända nycklar. Men för tillfället är det inte en del av språket.
Så den närmaste giltiga typen du kan skriva är
interface Yep {
readonly foo: "val1";
readonly bar: "val2";
readonly [all: string]: "val1" | "val2" | undefined;
}
Detta kan vara tillräckligt bra för ditt användningsområde:
declare const key: string;
const myObject: Yep = { foo: 'val1', bar: 'val2' };
const r = myObject.foo; // const r: "val1"
const s = myObject[key]; // const s: "val1" | "val2" | undefined
const t = myObject.baz; // const t: "val1" | "val2" | undefined 🤷♂️
Om det är tillräckligt bra kan du skriva en hjälpfunktion för att automatiskt ge ett objektliteral en sådan typ:
const exactish = <const T,>(
x: T & { [all: string]: T[keyof T] | undefined }
) => x
där snittet kombinerar inmatningstypen T med en indexsignatur av den lämpliga egenskapstypen, och där jag använder en const-typparameter för att avlasta anroparen från behovet att använda ett const-påstående:
const myObject = exactish({
foo: 'val1',
bar: 'val2'
});
/* const myObject: {
foo: "val1";
bar: "val2";
} & {
[all: string]: "val1" | "val2" | undefined;
} */
Det är en motsvarande typ till Yep ovan, och den beter sig därför på samma sätt, för bättre eller sämre:
const r = myObject.foo; // const r: "val1"
const s = myObject[key]; // const s: "val1" | "val2" | undefined
const t = myObject.baz; // const t: "val1" | "val2" | undefined
Lekplatslänk till kod
Svar #2
Du kan använda en egen typvakt med en typ-predikat för att kontrollera om key faktiskt är en nyckel till myObject. På det sättet undviker du direkta as-assertioner med TypeScripts styrflödesavsmalning. Genom att anropa typvakten i ett villkor kan du returnera undefined om key inte är någon keyof myObject.
result kommer att härledas som 'val1' | 'val2' | undefined som du förväntade dig.
const myObject = {
foo: 'val1',
bar: 'val2'
} as const;
type Keys = keyof typeof myObject;
const getKey = () => (Math.random() < 0.5 ? "foo" : "bar") as string;
const key = getKey();
function isKeyOf<T extends Record<any, any>>(obj: T, key: PropertyKey): key is keyof T {
return key in obj;
}
const result = isKeyOf(myObject, key) ? myObject[key] : undefined;
// ^? const result: "val1" | "val2" | undefined
TypeScript-lekplats
