TypeScript 泛型:泛型属性的联合
TypeScript generics: union of generic properties
我有一个步骤的概念。它需要一个输入并产生一个输出。
这是一个演示问题的简化示例(因此它可能缺少一些具有正确类型限制等的额外内容)。我们对 andThen
函数感兴趣,它在执行步骤时累积状态。本段代码无疑问,仅供参考
type SubType<T, U> = T extends U ? U : never;
class Step<A, B> {
constructor(private readonly f: (a: A) => B) { }
public run(a: A): B {
return this.f(a);
}
public andThen<C, D>(nextStep: Step<SubType<B, C> | B, D>): Step<A, B & D> {
return new Step<A, B & D>((state: A) => {
const b = this.f(state);
return { ...b, ...nextStep.run(b) };
});
}
}
以下工作正常,我们有两个步骤,自动推断类型,我们最终得到 all
预期类型 { user1: User, user2: User }
type User = { id: number, name: string };
const user1 = new Step((input: {}) => ({ user1: { id: 1, name: "A" } }));
const user2 = new Step((input: {}) => ({ user2: { id: 2, name: "B" } }));
const all = user1.andThen(user2).run({})
我的下一步是在处理产生结果的相同函数时重用相同的抽象,但我想要的唯一区别是结果所在的键。你可能不明白我刚才说的,所以让我给你看代码:
class UserStep<K extends string> extends Step<{}, { [k in K]: User }> { }
const user21 = new UserStep<"user1">((input: {}) => ({ user1: { id: 1, name: "A" } }));
const user22 = new UserStep<"user2">((input: {}) => ({ user2: { id: 2, name: "B" } }));
// inferred type here is wrong { user1: User } instead of { user1: User, user2: User }
const all2 = user21.andThen(user22).run({})
UserStep
应该处理用户的创建(或它需要做的任何其他工作),我希望它 return 具有键 K extends string
的对象中的结果。再一次,所有类型都自动检查并且似乎正常工作但是 all2
存在问题,其中类型被推断 不正确 (很可能它是正确的,但它不同于预期的)。我知道这很可能与 UserStep
的定义方式有关,它的键是 K extends string
但我看不出我可以采取什么其他方法来实现这个 UserStep
按预期工作。
所以问题是:
有没有一种方法可以抽象 UserStep
以便它 return 使用不同的键可以得到任何它想要的结果,以便在使用 andThen
类型检查器编写步骤后推断出 correct/expected 类型?
UPDATE:以下错误已在最新版本的 TypeScript 中 fixed 并且很可能与 TS3.8 一起发布。到那时,您应该可以只使用下面 Step<A, B>
的 "most reasonable typing",无需联合或 SubType
解决方法,一切都应该正常工作。
这里的根本问题是类型检查器没有意识到 Step<A, B>
的最合理类型,即:
interface Step<A, B> {
f: (a: A) => B;
andThen<C>(next: Step<B, C>): Step<A, B & C>;
}
在A
中是contravariant。这意味着 Step<T, B>
可分配给 Step<U, B>
当且仅当 U
可分配给 T
。可分配方向的开关是 "contra" 部分。如果方向相同,如“F<T>
可分配给 F<U>
当且仅当 T
可分配给 U
”,它将是 协变.
无论如何,这意味着你在这里得到一个错误:
declare const u1: Step<{}, { user1: {} }>;
declare const u2: Step<{}, { user2: {} }>;
let u1u2 = u1.andThen(u2); // error!?
// Type 'Step<{}, any>' is not assignable to type 'Step<{ user1: {}; }, any>'.
// Type '{}' is not assignable to type '{ user1: {}; }'.
它没有意识到 Step<A, B>
在 A
中是逆变的原因是因为 Step<A, B>
的 andThen()
方法涉及 Step
本身,并且在检查此类递归泛型类型中类型参数的方差时,类型检查器假定它是协变的,如 checker.ts
:
中 typeArgumentsRelatedTo()
函数内的注释中所述
// When variance information isn't available we default to covariance
(多亏了jack-williams for his helpful comment)
这是错误或设计限制;理想情况下,编译器不会做出任何默认猜测,而是检查结构兼容性。我认为围绕这个特定问题没有任何现有的 GitHub 问题,所以我已经提交 microsoft/TypeScript#35805 to ask about it. The general lack of ability for developers to add variance hints is covered in microsoft/TypeScript#1394。
无论如何,为了解决这个问题,您似乎使用了一种变通方法来使 andThen()
方法在仍然与 A
逆变相关时不会触发方差检查器:
type SubType<T, U> = T extends U ? U : never;
interface Step<A, B> {
f: (a: A) => B;
andThen<C, X>(next: Step<B | SubType<B, X>, C>): Step<A, B & C>;
}
declare const u1: Step<{}, { user1: {} }>;
declare const u2: Step<{}, { user2: {} }>;
let u1u2 = u1.andThen(u2); // okay
不幸的是,如您所见,面对像 SubType
这样的通用条件类型,类型推断可能会很混乱:
type User = { id: number, name: string };
interface UserStep<K extends string> extends Step<{}, { [k in K]: User }> { }
declare const v1: UserStep<"user1">;
declare const v2: UserStep<"user2">;
let v1v2 = v1.andThen(v2); // Step<{}, {user1: User;}> !!
最简单的 "fix" 就是手动指定它不能正确推断的类型参数:
let v1v2Fixed = v1.andThen<{ user2: User }, {}>(v2); // okay:
// Step<{}, { user1: User;} & { user2: User;}>
我 认为 的另一种解决方法在逆变方面具有相同的行为:
interface Step<A, B> {
f: (a: A) => B;
andThen<C, X>(next: Step<B | X, C>): Step<A, B & C>;
}
但是这里没有泛型条件类型需要担心,而且编译器在推断方面要好得多:
let v1v2 = v1.andThen(v2); // okay:
// Step<{}, { user1: User;} & { user2: User;}>
所以这可能是我的建议。不过,你真的应该测试一下,因为经常会出现疯狂的边缘情况。希望方差问题最终会被修复 "the right way",但至少现在你有一些选择。
好的,希望对你有帮助;祝你好运!
我有一个步骤的概念。它需要一个输入并产生一个输出。
这是一个演示问题的简化示例(因此它可能缺少一些具有正确类型限制等的额外内容)。我们对 andThen
函数感兴趣,它在执行步骤时累积状态。本段代码无疑问,仅供参考
type SubType<T, U> = T extends U ? U : never;
class Step<A, B> {
constructor(private readonly f: (a: A) => B) { }
public run(a: A): B {
return this.f(a);
}
public andThen<C, D>(nextStep: Step<SubType<B, C> | B, D>): Step<A, B & D> {
return new Step<A, B & D>((state: A) => {
const b = this.f(state);
return { ...b, ...nextStep.run(b) };
});
}
}
以下工作正常,我们有两个步骤,自动推断类型,我们最终得到 all
预期类型 { user1: User, user2: User }
type User = { id: number, name: string };
const user1 = new Step((input: {}) => ({ user1: { id: 1, name: "A" } }));
const user2 = new Step((input: {}) => ({ user2: { id: 2, name: "B" } }));
const all = user1.andThen(user2).run({})
我的下一步是在处理产生结果的相同函数时重用相同的抽象,但我想要的唯一区别是结果所在的键。你可能不明白我刚才说的,所以让我给你看代码:
class UserStep<K extends string> extends Step<{}, { [k in K]: User }> { }
const user21 = new UserStep<"user1">((input: {}) => ({ user1: { id: 1, name: "A" } }));
const user22 = new UserStep<"user2">((input: {}) => ({ user2: { id: 2, name: "B" } }));
// inferred type here is wrong { user1: User } instead of { user1: User, user2: User }
const all2 = user21.andThen(user22).run({})
UserStep
应该处理用户的创建(或它需要做的任何其他工作),我希望它 return 具有键 K extends string
的对象中的结果。再一次,所有类型都自动检查并且似乎正常工作但是 all2
存在问题,其中类型被推断 不正确 (很可能它是正确的,但它不同于预期的)。我知道这很可能与 UserStep
的定义方式有关,它的键是 K extends string
但我看不出我可以采取什么其他方法来实现这个 UserStep
按预期工作。
所以问题是:
有没有一种方法可以抽象 UserStep
以便它 return 使用不同的键可以得到任何它想要的结果,以便在使用 andThen
类型检查器编写步骤后推断出 correct/expected 类型?
UPDATE:以下错误已在最新版本的 TypeScript 中 fixed 并且很可能与 TS3.8 一起发布。到那时,您应该可以只使用下面 Step<A, B>
的 "most reasonable typing",无需联合或 SubType
解决方法,一切都应该正常工作。
这里的根本问题是类型检查器没有意识到 Step<A, B>
的最合理类型,即:
interface Step<A, B> {
f: (a: A) => B;
andThen<C>(next: Step<B, C>): Step<A, B & C>;
}
在A
中是contravariant。这意味着 Step<T, B>
可分配给 Step<U, B>
当且仅当 U
可分配给 T
。可分配方向的开关是 "contra" 部分。如果方向相同,如“F<T>
可分配给 F<U>
当且仅当 T
可分配给 U
”,它将是 协变.
无论如何,这意味着你在这里得到一个错误:
declare const u1: Step<{}, { user1: {} }>;
declare const u2: Step<{}, { user2: {} }>;
let u1u2 = u1.andThen(u2); // error!?
// Type 'Step<{}, any>' is not assignable to type 'Step<{ user1: {}; }, any>'.
// Type '{}' is not assignable to type '{ user1: {}; }'.
它没有意识到 Step<A, B>
在 A
中是逆变的原因是因为 Step<A, B>
的 andThen()
方法涉及 Step
本身,并且在检查此类递归泛型类型中类型参数的方差时,类型检查器假定它是协变的,如 checker.ts
:
typeArgumentsRelatedTo()
函数内的注释中所述
// When variance information isn't available we default to covariance
(多亏了jack-williams for his helpful comment)
这是错误或设计限制;理想情况下,编译器不会做出任何默认猜测,而是检查结构兼容性。我认为围绕这个特定问题没有任何现有的 GitHub 问题,所以我已经提交 microsoft/TypeScript#35805 to ask about it. The general lack of ability for developers to add variance hints is covered in microsoft/TypeScript#1394。
无论如何,为了解决这个问题,您似乎使用了一种变通方法来使 andThen()
方法在仍然与 A
逆变相关时不会触发方差检查器:
type SubType<T, U> = T extends U ? U : never;
interface Step<A, B> {
f: (a: A) => B;
andThen<C, X>(next: Step<B | SubType<B, X>, C>): Step<A, B & C>;
}
declare const u1: Step<{}, { user1: {} }>;
declare const u2: Step<{}, { user2: {} }>;
let u1u2 = u1.andThen(u2); // okay
不幸的是,如您所见,面对像 SubType
这样的通用条件类型,类型推断可能会很混乱:
type User = { id: number, name: string };
interface UserStep<K extends string> extends Step<{}, { [k in K]: User }> { }
declare const v1: UserStep<"user1">;
declare const v2: UserStep<"user2">;
let v1v2 = v1.andThen(v2); // Step<{}, {user1: User;}> !!
最简单的 "fix" 就是手动指定它不能正确推断的类型参数:
let v1v2Fixed = v1.andThen<{ user2: User }, {}>(v2); // okay:
// Step<{}, { user1: User;} & { user2: User;}>
我 认为 的另一种解决方法在逆变方面具有相同的行为:
interface Step<A, B> {
f: (a: A) => B;
andThen<C, X>(next: Step<B | X, C>): Step<A, B & C>;
}
但是这里没有泛型条件类型需要担心,而且编译器在推断方面要好得多:
let v1v2 = v1.andThen(v2); // okay:
// Step<{}, { user1: User;} & { user2: User;}>
所以这可能是我的建议。不过,你真的应该测试一下,因为经常会出现疯狂的边缘情况。希望方差问题最终会被修复 "the right way",但至少现在你有一些选择。
好的,希望对你有帮助;祝你好运!