怎么每次碰到想不通的代码行都出在 TypeScript😢 这行到底哪里有问题?

61 天前
 june4

服了 TypeScript 这货了,符合我对微软的刻板印象,最近几年似乎每次碰到看不明白的语法出错,都是在 TypeScript ,这货无数细节太微妙了。

interface Elements {
  div: { id: number; name?: string; color: string; x: string }
  a: { id: number; name?: string; href: string; x: string }
}

declare function getProps<T extends Record<any, any>>(obj: T, keys: (keyof T)[]): void

function Foo<T extends 'div' | 'a'>(tag: T, props: Omit<Elements[T], 'x'>) {
  // 这行报错 Type 'string' is not assignable to type 'Exclude<keyof Elements[T], "x">'
  getProps(props, ['id'])
}
Foo('a', { id: 1, href: '#' })

// 我以为是因为 Omit 只能放单个对象,试了下多个也行啊。
type kkk = Omit<Elements['div' | 'a'], 'x'>
把 `function Foo<T extends 'div' | 'a'>(tag: T, props: Omit<Elements[T], 'x'>)` 
改成 `function Foo<T extends 'div' | 'a'>(tag: T, props: Omit<Elements['div' | 'a'], 'x'>)` 就不会报错。
那这个 `T` 不就是 `'div' | 'a'` 吗?这 tm 二者到底有什么微妙区别导致代码一个报错一个没错?
1601 次点击
所在节点    TypeScript
10 条回复
ZZITE
61 天前
我觉得应该在保持第一种写法的情况下,在 getProps 传参这里使用断言处理,即 getProps(props, ["id"] as (keyof Omit<Elements[T], 'x'>)[]);

和 Omit 无关,两种情况的区别在于第一种 Omit<Elements[T], 'x'>的结果是泛型参数,这里 TS 是不会做出“ ‘id’一定存在于这个泛型中”的这种推断的,它无法确定具体的类型结构。这里的报错具体来说其实是“ “id' is not assignable to type 'Exclude<keyof Elements[T], "x">'”。但我们知道"id"是有效的 key😂,所以在传入“id”的时候对它做个类型断言。

至于第二种写法,Omit<Elements['div' | 'a']是一个具体的联合类型,ts 是可以确定整个类型结构的,也可以推断出"id"是一个符合联合类型结果的有效 key
june4
61 天前
@ZZITE 可是这个 T 已经在函数类型参数声名中限制只能是 'div' | 'a' 了啊?'id'是一定存在于这个 props 里的。
june4
61 天前
@june4 这里的问题是 Omit<Elements[T], 'x'> 这里的 Omit 操作结果出问题了。如果去掉 Omit ,直接用 Elements[T],那一点问题都没有。 但这个 Omit 我似乎也看不出为什么会出问题?
duli950523
61 天前
@june4 #2 ts 能力是由限的,你觉得是,但是它处理起来就比较麻烦,这也是 as 的作用之一,用在你比编译器知道更多的信息,一般给泛型套上另一个 Conditional Type 大部分就不会继续计算了,所以没必要纠结这些
csl123
61 天前
Omit 改成 Exclude 好像就可以了,原因没细看。

``` typescript

interface Elements {
div: {
id: number; name?: string; color: string; x: string
}
a: { id: number; name?: string; href: string; x: string }
}

type E<T extends 'div' | 'a'> = Exclude<keyof Elements[T], 'x'>

// type EDiv = "id" | "name" | "color"
type EDiv = E<'div'>
// type EA = "id" | "name" | "href"
type EA = E<'a'>



type O<T extends 'div' | 'a'> = Omit<keyof Elements[T], 'x'>

/**
* type ODiv = {
toString: () => string;
valueOf: () => string;
toLocaleString: () => string;
}
*/
type ODiv = O<'div'>
/**
* type OA = {
toString: () => string;
valueOf: () => string;
toLocaleString: () => string;
}
*/
type OA = O<'a'>
```
ZZITE
61 天前
@june4 #3 嗯嗯,这里 Elements[T]如你所说,通过 T extends 'div' | 'a' 是可以推断出具体类型结构的,但在 Omit 后返回了一个新类型,这个新类型是抽象的,就是我说的泛型参数,这里是无法推断出 id 一定存在于新类型的类型结构里的。
june4
61 天前
@ZZITE 也许是这样。
我正在读一个库的代码,这个是我自己抽出来改造的一部分实验代码,我这方法行不通。我的类型和库的不同,库的明显更繁琐,我突然有点理解为啥库要舍近求远那么设计类型了, 也许就是因为不那么干是条死路🐶
哎看大能的代码有一点不爽就是看不到别人的思路过程只看到最终结果。
zhangdroid
61 天前
可以看看这个 issue: https://github.com/microsoft/TypeScript/issues/39556

这样就可以 work 了:

```ts
interface Elements {
div: { id: number; name?: string; color: string; x: string }
a: { id: number; name?: string; href: string; x: string }
}

declare function getProps<T extends Record<any, any>>(obj: T, keys: (keyof T)[]): void

type SimpleUnionOmit<T, K extends string | number | symbol> = T extends unknown ? Omit<T, K> : never

function Foo<T extends 'div' | 'a'>(tag: T, props: SimpleUnionOmit<Elements[T], 'x'>) {
getProps(props, ['id'])
}
```
shizhibuyu2023
61 天前
any 、 @ts-ignore 梭到底
cosmos601
61 天前
写这种复杂函数的 ts 类型是真的想死,半小时写完的业务代码,搞类型半小时

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1069519

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX