为什么TypeScript类型推断在调用函数时有效,但在内部无效?

4sup72z8  于 2023-04-13  发布在  TypeScript
关注(0)|答案(2)|浏览(130)

在下面的例子中,TypeScript能够在调用getObjPath函数时正确地推断参数,但在内部,它在satisfies ObjPaths上失败了。有人能解释一下吗?或者给我指一个文档,我可以理解发生了什么?

const obj = {
  a: '1',
  b: '2',
} as const;

type Obj = typeof obj;
type ObjKeys = keyof Obj;
type ObjPaths = { [K in ObjKeys]: `${K} | ${Obj[K]}`}[ObjKeys];

const getObjPath = <K extends ObjKeys, V extends Obj[K]>(k: K, v: V) => {
  return `${k} | ${v}` satisfies ObjPaths; // unable to correctly infer
}

getObjPath('a', '2'); // correctly infers
o2rvlv0m

o2rvlv0m1#

这里的一般问题是,TypeScript并没有一个很好的方法来表示 * 相关的联合类型 * 或限制到这种联合的generics。如果你有多个依赖于同一个联合类型值的值或表达式,那么编译器会错误地认为该表达式的每个外观都是独立的。特别是对于template literal types,这在microsoft/TypeScript#49132中讨论过。但是在X1 E3 F1 X中详细描述了一般问题。
例如,让我们看看ObjKeys类型的值会发生什么:

function foo(k: ObjKeys) {
  const p = `${k} | ${obj[k]}` as const;
  // const p: "a | 1" | "a | 2" | "b | 1" | "b | 2"; ☹
}

在这里,p的类型被编译器推断为"a | 1" | "a | 2" | "b | 1" | "b | 2",就好像kobj[k]的类型彼此独立一样。p实际上不可能是"a | 2""b | 1",但编译器认为它可能是,如果你试图将它赋值给ObjPaths类型的变量,你会遇到麻烦。
你希望编译器做的事情更像这样:

function foo(k: ObjKeys) {
  const q = k === "a" ?
    `${k} | ${obj[k]}` as const :
    `${k} | ${obj[k]}` as const;
  // const q: "a | 1" | "b | 2"
}

其中依次考虑k的每种可能性,然后将结果合并在一起。当您手动执行冗余操作时,这是可行的,但当您将其写入一行时则不行。要求编译器像**一样单独执行每种情况会很好,如decline microsoft/TypeScript#25051中所述,但这是不可能的。
这就是这里的局限性。相关的联合处理得不是很好。幸运的是,在microsoft/TypeScript#47109中描述了一个推荐的修复,在那里我们重构成一个 * 分布式对象类型 * 和泛型。你的代码实际上非常接近;我们需要做的就是在特定的键中使ObjPaths成为泛型,并使函数也返回一个泛型:

type ObjPaths<K extends ObjKeys> =
  { [P in K]: `${P} | ${Obj[P]}` }[K];

const getObjPath = <K extends ObjKeys>(k: K, v: Obj[K]) => {
  return `${k} | ${v}` satisfies ObjPaths<K>; // okay
}

(Note我删除了额外的类型参数V除非你实际上有V的不同可能性,否则你可能不想让函数变得比它需要的更通用而使事情复杂化。)事实上,你可以将返回类型注解为ObjPaths<K>,而不会丢失任何你关心的信息:

const getObjPath = <K extends ObjKeys>(k: K, v: Obj[K]): ObjPaths<K> => {
  return `${k} | ${v}`; // okay
}

这仍然表现为调用方所需的行为:

const ok1 = getObjPath('a', '1'); 
// const ok1: "a | 1"
const ok2 = getObjPath('b', '2');
// const ok2: "b | 2"
getObjPath('a', '2'); // error! '2' is not '1'

Playground链接到代码

u59ebvdq

u59ebvdq2#

getObjPath('a', '1'); // correctly infers
getObjPath('b', '2'); // correctly infers

但你写的不是预期的类型。
试试这个:

const obj = {
  a: '1',
  b: '2'
} as const;

type Obj = typeof obj;
type ObjKeys = keyof Obj;
type ObjVals = Obj[ObjKeys];
type ObjPaths = { [K in ObjKeys]: `${K} | ${ObjVals}`}[ObjKeys];

const getObjPath = <K extends ObjKeys, V extends ObjVals>(k: K, v: V) => {
  return `${k} | ${v}` satisfies ObjPaths; // unable to correctly infer
};

getObjPath('a', '2'); // correctly infers

相关问题