typescript - Varför fungerar inte min villkorliga typ som jag förväntar mig?
class A {}
class B {}
class C {}
class D {}
//! How/Why does that even work?
type MyObjectType<T extends (keyof Hash), Hash> = (keyof Hash) extends T ? null : Hash[T];
class Example {
public static ClassesByName = { A: A, B: B, C: C, D: D };
public stringToClass<
Hash extends typeof Example.ClassesByName,
T extends keyof Hash,
>(className: T): MyObjectType<T, Hash> {
throw new Error("unimplemented");
}
}
const instance = new Example();
const klassA = instance.stringToClass("A"); //<- Expected: klassA is "typeof A" as wanted
const klassB = instance.stringToClass("B"); //<- Expected: klassB is "typeof B" as wanted
const klassZ = instance.stringToClass("Z"); //<- Expected: klassZ is null, wrong parameter triggers ts error as wanted
Lekplats
Det ser ut som att min villkorliga typ fungerar tvärtom mot dokumentationen
TypeScripts dokumentation för villkorliga typer säger:
SomeType extends OtherType ? TrueType : FalseType;
Därför är min logik: om keys i Hash innehåller key (T), då Hash[T] annars null; så jag skrev:
type MyObjectType<T extends (keyof Hash), Hash> = (keyof Hash) extends T ? Hash[T] : null;
Men det fungerade inte och att vända TrueType : FalseType gör att det fungerar perfekt:
type MyObjectType<T extends (keyof Hash), Hash> = (keyof Hash) extends T ? null : Hash[T];
Varför fungerar det i motsatt riktning?
Svar #1
Det verkar som att du är förvirrad över betydelsen av nyckelordet extends både i generiska begränsningar och i villkorliga typer. Om du har två typer X och Y, betyder det att att säga X extends Y att om du har ett värde av typen X är det tillåtet att tilldela det till en variabel av typen Y, men inte nödvändigtvis tvärtom.
Så om du vill säga att du har ett värde av typen K som bör kunna tilldelas en variabel av typen keyof H. Tänk dig till exempel att H är {a: string, b: number, c: boolean}. Då har du ett värde av typen K som bör kunna tilldelas en variabel av unionstypen "a" | "b" | "c". Detta kommer låta K vara "a", eller "b", eller "a" | "b", men om det är något som "z" så kommer det inte att fungera. I det här fallet behöver du säga K extends keyof H. Om du säger motsatsen, att keyof H extends K, säger du att K måste acceptera alla av "a" | "b" | "c", åtminstone. Men det är inte vad du försöker göra.
Om du fortfarande tycker att extends borde fungera åt andra hållet, betyder det förmodligen att du tänker på objekttyper och inte nyckeltyper. Objekt är kontravarianta i sina nyckeltyper, vilket betyder att om A extends B, så gäller keyof B extends keyof A. Se Union och Intersection av typer för ett liknande problem.
Så din typ
type MyObjectType<K extends keyof H, H> = (keyof H) extends K ? null : H[K];
fungerar bara någonsin ”av en slump”. Eftersom K extends keyof H, är det enda sättet att (keyof H) extends K om K och keyof H är samma. Det betyder att K måste vara unionen av alla nycklar i H. Detta händer vanligtvis inte; om du anropar stringToClass() med en av de giltiga nycklarna, kommer K att bara vara den nyckeln, och kontrollen (keyof H) extends K kommer misslyckas, och du kommer få H[K]. Å andra sidan, om någon anropar stringToClass() med något ogiltigt, kommer inferensen för K att misslyckas, den faller tillbaka till den fullständiga begränsningen keyof H, kontrollen (keyof H) extends K kommer lyckas, och du kommer få null. Att detta råkar fungera är en kombination av tillfälligheter, och vi bör helt enkelt skriva om din typ korrekt.
Så vi vill ha K extends keyof H och inte tvärtom. Och det betyder att du skulle kunna skriva type MyObjectType<K extends keyof H, H>=H[K] direkt utan en konditionell typ alls. Trots allt har du redan begränsat K extends keyof H. Om du måste göra en kontroll eftersom du vill returnera null för felaktiga inmatningar, då bör du öppna upp K för att tillåta sådana inmatningar och göra den konditionella kontrollen på rätt sätt:
type MyObjectType<K, H> = K extends keyof H ? H[K] : null
Och sedan ser stringToClass() ut som
class Example {
public static ClassesByName = { A: A, B: B, C: C, D: D };
public stringToClass<
H extends typeof Example.ClassesByName,
K,
>(className: K & keyof H): MyObjectType<K, H> {
throw new Error("unimplemented");
}
}
där jag igen tog bort begränsningen från K (så att MyObjectType<K, H> faktiskt tillåter det), och för att bevara felet när du kallar det med en dålig classname-input, skar jag K med keyof H.
Låt oss prova det:
const instance = new Example();
const klassA = instance.stringToClass("A"); // okay
// const klassA: typeof A
const klassB = instance.stringToClass("B"); // okay
// const klassB: typeof B
const klassZ = instance.stringToClass("Z"); // error!
// ---------------------------------> ~~~
// const klassZ: null
Ser bra ut.
Lekplatslänk till kod
