假设我有两种类型:

type PathBare<T> = keyof T & string
type PathWrapped<T> = `${keyof T & string}`

两者都只是作为一个联合体获取对象的字符串键.但是一个人在字符串字面量中插入字符串,另一个人不这样做.

它们应该是等价的,因为字符串文字的包装不应该在 struct 上改变类型.例如:

type ABC = 'a' | 'b' | 'c'
type A = ABC extends `${ABC}` ? true : false // true

但是,当我向这些类型传递泛型类型而不是具体类型时,发生了一些奇怪的事情:

type Named = { name: string, foo: string }

function foo<T extends Named>(arg: T): T {
    const a1: PathWrapped<Named> = 'name' // fine
    const a2: PathBare<Named> = 'name' // fine

    const b1: PathWrapped<T> = 'name' // Type 'string' is not assignable to type '`${keyof T & string}`'.(2322)
    const b2: PathBare<T> = 'name' // fine

    return arg
}

这里使用T应该与使用约束T的类型完全相同,但事实并非如此.它似乎完全忘记了它是什么,在没有强制转换的情况下不再是可赋值的.

为什么会这样呢?这里发生了什么事?

See Typescript Playground


这显然是一个人为的例子.库中的实际用例为虚线路径提供了一种类型,用于通过字符串标识嵌套字段.我已经减少了涉及的类型,以找到导致问题的最小情况.


鼓舞人心的例子:

import { useEffect } from 'react'
import { useForm } from 'react-hook-form'


type Named = { name: string }

function useFormWithNamedData<T extends Named>(data: T) {
    const { setValue } = useForm({ defaultValues: async () => data })

    useEffect(() => {
        setValue('name', 'foo') // Argument of type '"name"' is not assignable to parameter of type 'Path<T>'.(2345)
    }, [])

    return {setValue}
}
useFormWithNamedData({ name: 'a', foo: 'b' }).setValue('foo', 'c') // good

function useFormWithNamedDataNonGeneric(data: Named) {
    const { setValue } = useForm({ defaultValues: async () => data })

    useEffect(() => {
        setValue('name', 'foo') // fine
    }, [])

    return {setValue}
}
useFormWithNamedDataNonGeneric({ name: 'a', foo: 'b' }).setValue('foo', 'c') // error, needs to work

这里,react-hook-form中的setValue函数期望设置属性的字符串路径.但'name'是不允许的,即使'name'是约束的一部分,因此它必须存在.看来编译器应该知道这一点.

我不能在调用useForm时直接使用约束,因为我想返回依赖于T的类型的数据.在这种情况下,setValue将接受'name'以及传入该对象的任何其他路径.

在试图找出原因的过程中,我把这个问题简化为这个问题的顶部.

See Playground

推荐答案

TypeScript在对generic个类型执行类型操作时,正确推理可以适当得出的结论的能力有限. 给定某个类型函数type F<T extends U> = ⋯U⋯和一堆特定类型ABC等,但这并不意味着它将知道Farbitrary或泛型类型T做了什么. 最"明显"的情况是,通过构造,F<T extends U>是对所有T延伸U的同一操作. 这就是template literal type序列化一个字符串:

type Same<T extends string> = `${T}`;

当然,编译器可以得出结论,对于大约specificA,Same<A>A…它只有evaluatesit.否则,模板文字类型将是完全不透明的东西,根本没有人可以使用.但是:

function bar<T extends string>(t: T) {
    const sameA: Same<"a"> = "a"; // okay  
    const sameB: Same<"b"> = "b"; // okay
    const sameC: Same<"c"> = "c"; // okay
    const sameT: Same<T> = t; // error!
}

我们不能指望编译器理解它对泛型T这样做,仅仅是因为它理解如何计算每个单独的.编译器不能对特定类型的类型函数求值以得出关于其一般行为的新的更高阶结论的能力.

在编译器似乎可以做到这一点的地方,这只是因为TypeScrip被故意编程来做到这一点.这并没有发生在"通过模板文字重新序列化string"操作中,这可能是因为在现实世界的代码中几乎不需要这样的推理.

请注意,对于泛型模板文字,我找不到规范的GitHub问题.然而,"在将类型函数应用于泛型时,类型脚本看不到东西"这一更广泛的类别也存在相关问题.例如,microsoft/TypeScript#24560.


这就解释了为什么PathWrapped<T>不能像T那样通用. 没有人编写编译器来这样做. PathBare<T>does之所以能工作,是因为它们确实专门编程了,如果是T extends U,那么就是keyof U extends keyof T.否则,您永远无法使用其约束的任何键索引到泛型对象类型中,并且许多真实世界的代码需要这样做. 但是一旦你通过模板文字类型传递keyof T,这些知识就没用了.如果从PathWrapped<T>T的逻辑链中缺少任何一个链接,整个过程就会失败.

Playground link to code

Typescript相关问答推荐

使用新控制流的FormArray内部的Angular FormGroup

如何根据数据类型动态注入组件?

TypeScript类型不匹配:在返回类型中处理已经声明的Promise Wrapper

找不到带名称的管道''

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

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

带switch 的函数的返回类型的类型缩小

匿名静态类推断组成不正确推断

将带点的对象键重新映射为具有保留值类型的嵌套对象

常量类型参数回退类型

TYPE FOR ARRAY,其中每个泛型元素是单独的键类型对,然后在该数组上循环

有没有办法在Zod中使用跨字段验证来判断其他字段,然后呈现条件

打字错误推断

接受字符串或数字的排序函数

设置不允许相同类型的多个参数的线性规则

在TS泛型和记录类型映射方面有问题

基于闭包类型缩小泛型类型脚本函数的范围

Karma Angular Unit测试如何创建未解析的promise并在单个测试中解决它,以判断当时的代码是否只在解决后运行

为什么类方法参数可以比接口参数窄

使用react+ts通过props传递数据