Typescript工具类'Record'和'Partial'无法与受限制的泛型一起使用。

3
我需要基于受限制的泛型创建一个新类型。新类型应该与泛型具有相同的键,使它们变得可选,并将它们映射到数字。
我的第一种方法是使用 Partial<Record<keyof Entity, number>>,但由于某些原因导致出现错误 Type [...] is not assignable to type 'Partial<Record<keyof Entity, number>>'.
所以我退而求其次,手动编写类型映射 { [K in keyof Entity]?: number },这似乎可行。
interface Person {
  firstName: string;
  lastName: string;
}

export class SomeClass<Entity extends Person> {
  protected getFilter(): void {
    let prio1: { [K in keyof Entity]?: number };
    prio1 = { firstName: 1 };

    // Type '{ firstName: 2; }' is not assignable to type 'Partial<Record<keyof Entity, number>>'.
    let prio2: Partial<Record<keyof Entity, number>>;
    prio2 = { firstName: 2 };
  }
}

请问这两种符号有什么区别,为什么内置的Typescript工具无法使用?
在Typescript playground上也可以找到代码片段:请点击此处查看
我使用的资源包括:记录类型映射类型泛型
更新:
我尝试分解Typescript在内部执行的操作,逐步展开 Partial<Record<keyof Entity, number>>,并使用官方类型定义进行说明。
/**
 * Make all properties in T optional
 */
type Partial<T> = { [P in keyof T]?: T[P] };

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = { [P in K]: T };


class SomeClass<Entity extends Person> {
  getFilter(): void {
    let prio1: { [K in keyof Entity]?: number };
    prio1 = { firstName: 1 };

    let p1: Partial<Record<keyof Entity, number>>;
    let p2: Partial<{ [P in keyof Entity]: number }>;
    let p3: { [P2 in keyof { [P1 in keyof Entity]: number }]?: { [P1 in keyof Entity]: number }[P2] };
    let p4: { [P2 in keyof Entity]?: { [P1 in keyof Entity]: number }[P2] };
    let p5: { [P2 in keyof Entity]?: number };

    p1 = { firstName: 3 }; // error
    p2 = { firstName: 3 }; // error
    p3 = { firstName: 3 }; // error
    p4 = { firstName: 3 }; // error
    p5 = { firstName: 3 }; // works
  }
}

Typescript似乎无法将{ [P1 in keyof Entity]: number }[P2]中的 P2 in keyof Entity 解析为number

1个回答

0

首先,请查看这个答案。它应该会给你一些背景信息。

Entity视为Person的子类型。它不仅具有Person属性,还可能具有其他属性。

看这个例子:

interface Person {
    firstName: string;
    lastName: string;
}

type User = {
    premium: true
} & Person

UserPerson 的子类型,换句话说它继承了 Person

让我们创建一个用户对象:

interface Person {
    firstName: string;
    lastName: string;
}

type User = {
    premium: true
} & Person

let user: Record<keyof User, number> = {
    firstName: 1,
    lastName: 2,
    premium: 3
}

因此,我们的“Partial”用户可能有很多状态:

// just an alias
type PartiaUser = Partial<Record<keyof User, number>>

let partialUser1: PartiaUser = {}
let partialUser2: PartiaUser = { firstName: 1 }
let partialUser3: PartiaUser = { firstName: 1, lastName: 2, }
let partialUser4: PartiaUser = { lastName: 2, }
let partialUser5: PartiaUser = { lastName: 2, premium: 3, }

看看 partialUser5,它是一个没有 firstName 属性的对象。

让我们回到你的例子:

interface Person {
    firstName: string;
    lastName: string;
}

export class SomeClass<Entity extends Person> {
    getFilter(): void {
        let prio1: { [K in keyof Entity]?: number };
        prio1 = { firstName: 1 };

        let prio2: Partial<Record<keyof Entity, number>>;
        prio2 = { firstName: 2 };
    }
}

prior1 可以工作,因为在这种情况下,prior1 明确具有可选的 firstName 属性,因为 Entity 具有 Person 的所有键。

prior2 更有趣。

我认为问题在于 JavaScript 的动态性质。这只是我的观点。

看这个例子:

type User = {
    firstName: string;
    lastName: string;
    premium: true
} & string

type IsExtends<T> = T extends Person ? true : false

type Test = IsExtends<User>

问题在于上述的 User 类型仍然继承了 Person
interface Person {
    firstName: string;
    lastName: string;
}

type User = {
    firstName: string;
    lastName: string;
    premium: true
} & string

export class SomeClass<Entity extends Person> {
    getFilter(): void {
        let prio1: { [K in keyof Entity]?: number };
        prio1 = { firstName: 1 };

        let prio2: Partial<Record<keyof Entity, number>>;
        prio2 = { firstName: 2 };
    }
}
const result = new SomeClass<User>() // no error

事实上,User 是一个带有一些静态属性的字符串。
它可以在类型系统中表示,但在运行时无法呈现。
让我们通过一个小例子来测试它:
type PartialUser = Partial<Record<keyof User, number>>
declare var partialUser: PartialUser

partialUser = { firstName: 2 } // error

在这种情况下,问题出在toString方法中。请看一下在string原语中如何实现toString
// toString: () => string;
type ToString = Pick<{
    [Prop in keyof string]: string[Prop]
}, 'toString'>

正如您可能已经注意到的那样,toString是一个函数。我的意思是toString的值是一个函数。但是Record<keyof User, number>期望每个值都是一个number

让我们再次看看我们的例子:

interface Person {
    firstName: string;
    lastName: string;
}

type User = {
    firstName: string;
    lastName: string;
    premium: true;
    toString: number;
}

export class SomeClass<Entity extends Person> {
    getFilter(): void {
        let prio1: { [K in keyof Entity]?: number };
        prio1 = { firstName: 1 };
        // added User for experimenting with prio2 = { firstName: 2, toString: 42 }
        let prio2: Partial<Record<keyof User, number>>;
        prio2 = { firstName: 2 };
    }
}
const result = new SomeClass<User>() // no error

为了清晰起见,我已经显式添加了toString属性。

prio1 = { firstName: 1 } - 这是可行的,因为我们创建了一个新类型,其中每个值都是number

由于我们覆盖了内置方法,typescript声称该方法与默认的toString不兼容。

尝试使用prio2 = { firstName: 2, toString: 42 }。您会发现它会修复错误。

默认情况下,TS希望每个对象都有toString作为方法,并且这是预期的行为。但是如果您覆盖它-它会让TS感到不满意

我希望现在您明白为什么会出现错误。

extends - 不意味着 - equal

Playground

Related github issue


1
感谢您详细的回复。我跟着您的示例走,并同意您关于 toString 的分析:类型 () => stringnumber 不兼容。 但我不确定这是否是实际的根本原因。事实上,最后一个代码示例仅适用于 prio1,因为它的类型基于 Entitykeyof 而不是 Userkeyof。如果它基于 User,它也会导致 Type '() => string' is not assignable to type 'number'. - Elias Rabl
@Elias Rabl 我很高兴我的回答足够清晰。这对我来说也是一种进步 :D - captain-yossarian from Ukraine
请查看我的更新评论。我还更新了原始问题的最后一部分,以包括我对PartialRecord TS定义进行解糖的尝试。 - Elias Rabl
我不理解这个问题。 - captain-yossarian from Ukraine
在我看来,{ [K in keyof Entity]?: number } 应该等同于 Partial<Record<keyof Entity, number>>,因为后者只是一种简单的语法糖来实现相同的效果。手动分解第二个语句,使用 Typescript 中 PartialFilter 的内部类型定义,最终得到了预期的类型。但由于某些原因,Typescript 并没有以相同的方式解析它。它应该能够将 { [P1 in keyof Entity]: number }[P2] 简化为 number,因为 Entity 中的所有字段都映射到数字,并且 P2 表示 Entity 的所有键。 - Elias Rabl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接