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

Playground

看起来我的条件类型与文档相反

Typescript 的documentation for conditional types个国家:

SomeType extends OtherType ? TrueType : FalseType;

因此,我的逻辑是:如果Hash中的键包括key(T),则Hash[T]中包含键(T);否则为null;因此我写道:

type MyObjectType<T extends (keyof Hash), Hash> = (keyof Hash) extends T ? Hash[T] : null;

但这并没有用词,颠倒TrueType : FalseType个字让它完美地工作:

type MyObjectType<T extends (keyof Hash), Hash> = (keyof Hash) extends T ? null : Hash[T];

为什么它会起到相反的作用?

推荐答案

听起来你好像搞不清extends这个关键字在generic constraintsconditional types中的意思.如果你有两个类型XY,那么说X extends Y意味着如果你有一个X类型的值,你可以把它赋给Y类型的变量,但反之亦然.

所以如果你想说你有一个K类型的值,它应该可以赋值给keyof H类型的变量.比如,想象H等于{a: string, b: number, c: boolean}.然后,您有一个类型为K的值,它应该可以赋值给变量the union type "a" | "b" | "c".这将使K"a""b""a" | "b",但如果它类似于"z",则它不会起作用.在这种情况下,你需要说K extends keyof H.如果你说的相反,那keyof H extends K,你是说K至少需要接受"a" | "b" | "c"个中的all个.但这不是你想要做的.

如果你仍然认为extends应该以另一种方式工作,这可能意味着你正在考虑对象类型而不是键类型. 对象的键类型是contravariant,这意味着如果是A extends B,则是keyof B extends keyof A. 第Union and Intersection of types章类似的问题


所以你喜欢的类型

type MyObjectType<K extends keyof H, H> = (keyof H) extends K ? null : H[K];

只有在偶然的情况下才会"奏效".从K extends keyof H开始,唯一的方法(keyof H) extends K是如果Kkeyof H相同.这意味着K必须是H中所有密钥的并集.这通常不会发生;如果您使用一个有效的密钥调用stringToClass(),那么K将只是那个密钥,而支票(keyof H) extends K将是fail,您将得到H[K].另一方面,如果有人用无效的东西调用stringToClass(),K的推理将失败,它将回退到完全约束keyof H,判断(keyof H) extends Ksucceed,而您将得到null.这种情况发生在某种工作上是偶然的组合,我们应该正确地重写您的类型.


所以我们想要K extends keyof H,而不是相反.这意味着您可以直接编写type MyObjectType<K extends keyof H, H> = H[K],而不需要任何条件类型.毕竟,你已经限制了K extends keyof H.如果您必须执行判断,因为您希望为错误的输入返回null,那么您应该取消对K的限制以允许此类输入,并以正确的方式执行条件判断:

type MyObjectType<K, H> = K extends keyof H ? H[K] : null

然后stringToClass()看起来就像

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");
  }
}

我再次从K中删除了约束(因此MyObjectType<K, H>实际上允许它),并在使用错误的classname输入调用它时保留错误,I intersected K使用keyof H.


让我们来测试一下:

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

看上go 不错.


Playground link to code

Typescript相关问答推荐

调用另一个函数的函数的Typescript类型,参数相同

脉轮ui打字脚本强制执行图标按钮的咏叹调标签-如何关闭

typescribe不能使用值来索引对象类型,该对象类型满足具有该值的另一个类型'

如何设置绝对路径和相对路径的类型?

TypeScrip:基于参数的条件返回类型

当方法参数和返回类型不相互扩展时如何从类型脚本中的子类推断类型

ANGLE找不到辅助‘路由出口’的路由路径

TS2339:类型{}上不存在属性消息

诡异的文字类型--S为物语的关键

在Typescript 中,有没有`index=unfined的速记?未定义:某个数组[索引]`?

以Angular 自定义快捷菜单

使用2个派生类作为WeakMap的键

在打字脚本中对可迭代对象进行可变压缩

为什么上下文类型在`fn|curred(Fn)`的联合中不起作用?

创建一个TypeScrip对象并基于来自输入对象的约束定义其类型

如何从接口中省略接口

在抽象类构造函数中获取子类型

为什么数字是`{[key:string]:UNKNOWN;}`的有效密钥?

来自枚举的<;复选框和组件列表

如何正确键入泛型相对记录值类型?