英文:
Typescript strict-typing and inferred generic types (propagated)
问题
I am trying to make my hobby TS elegant and tight... but it's not going well.
Typescript experts, I've battled this for a few days, and would like your wisdom.
TL;DR; Drop the self-contained sample into VSCode and see compile errors.
I'm establishing my messaging platform, and each Message
class has strictly-typed message.msg
and message.satisfy
members.
I'm hoping to establish those Message classes' type constraints so that the generic types of each receiving method infers their inner types and keep the strict-typing all the way down... but unknown
is laughing at me.
Note:
- GOOD: The
NameAgeMessage
constructor argument is strongly-typed. - BAD: The
message
passed to the generic method doesn't infer types properly. - BAD: The
message
passed to the generic method doesn't infer types properly. - BAD: The
message
passed to the generic method doesn't infer types properly. - BAD: Both received arguments are
unknown
.
Also:
- If I add
= any
to my inner types it compiles, but I lose strict typing:
public publishForSatisfy<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg = any, TSatisfyArg = any>(
My best attempt so far:
(Scroll down to see the points & errors in comments)
type NameAgeMsg = { id: number };
type NameAgeSatisfyArg = { name: string, age: number };
abstract class AppMessage<TMsg, TSatisfyArg> {
abstract readonly msg: TMsg;
satisfy!: undefined | ((msg: TMsg, satisfyArg: TSatisfyArg) => void);
protected constructor() { }
};
class NameAgeMessage extends AppMessage<NameAgeMsg, NameAgeSatisfyArg> {
constructor(
public readonly msg: NameAgeMsg
) {
super();
}
}
class LoggingService {
public log(callback: () => { message: string, args?: object[] }) {
const logArgs = callback();
console.log(`${logArgs.message}`, ...logArgs.args ?? []);
}
}
function outputDebugInfoForMessagePublished<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg, TSatisfyArg>(
logging: LoggingService,
message: TMessage): void {
logging?.log(() => { return { message: `${message.constructor.name}`, args: [message] }; });
}
function messagingPublish<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg, TSatisfyArg>(
message: TMessage,
logging: LoggingService
): void {
// 4. BAD: The message passed to the generic method doesn't infer types properly.
// ts(2345) on 'message' argument: Type 'AppMessage<TMsg, TSatisfyArg>' is not assignable to type 'AppMessage<unknown, unknown>'.
outputDebugInfoForMessagePublished(logging, message);
}
class MessagingService {
private logging: LoggingService = new LoggingService();
public publishForSatisfy<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg, TSatisfyArg>(
message: TMessage,
satisfy: (msg: TMsg, satisfyArg: TSatisfyArg) => void
): void {
message.satisfy = satisfy;
// 3. BAD: The message passed to the generic method doesn't infer types properly.
// ts(2345) on 'message' argument: Type 'AppMessage<TMsg, TSatisfyArg>' is not assignable to type 'AppMessage<unknown, unknown>'.
messagingPublish(message, this.logging);
}
}
class TestClass {
messaging: MessagingService = new MessagingService();
testMethod(): void {
// 1. GOOD: The NameAgeMessage constructor argument is strongly-typed.
const message = new NameAgeMessage({ id: 123 });
// 2. BAD: The message passed to the generic method doesn't infer types properly.
// ts(2345) on 'message' argument: Argument of type 'NameAgeMessage' is not assignable to a parameter of type 'AppMessage<unknown, unknown>'.
this.messaging.publishForSatisfy(message, (msg, satisfyArg) => {
// 5. BAD: Both received arguments are 'unknown'.
// msg is 'unknown'. Should be type NameAgeMsg (from NameAgeMessage)
// satisfyArg is 'unknown'. Should be type NameAgeSatisfyArg (from NameAgeMessage)
});
}
}
英文:
I am trying to make my hobby TS elegant and tight... but it's not going well.
Typescript experts, I've battled this for a few days, and would like your wisdom.
TL;DR; Drop the self-contained sample into VSCode and see compile errors.
I'm establishing my messaging platform, and each Message
class has strictly-typed message.msg
and message.satisfy
members.
I'm hoping to establish those Message classes' type constraints so that the generic types of each receiving method infers their inner types and keep the strict-typing all the way down... but unknown
is laughing at me.
Note:
- GOOD: The
NameAgeMessage
constructor argument is strongly-typed. - BAD: The
message
passed to generic method doesn't infer types properly. - BAD: The
message
passed to generic method doesn't infer types properly. - BAD: The
message
passed to generic method doesn't infer types properly. - BAD: Both received arguments are
unknown
.
Also:
- If I add
= any
to my inner types it compiles, but I loose strict typing:
public publishForSatisfy<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg = any, TSatisfyArg = any>(
My best attempt so far:
(Scroll down to see the points & errors in comments)
type NameAgeMsg = { id: number };
type NameAgeSatisfyArg = { name: string, age: number};
abstract class AppMessage<TMsg, TSatisfyArg> {
abstract readonly msg: TMsg;
satisfy!: undefined | ((msg: TMsg, satisfyArg: TSatisfyArg) => void);
protected constructor() { }
};
class NameAgeMessage extends AppMessage<NameAgeMsg, NameAgeSatisfyArg> {
constructor(
public readonly msg: NameAgeMsg
) {
super();
}
}
class LoggingService {
public log(callback: () => { message: string, args?: object[] }) {
const logArgs = callback();
console.log(`${logArgs.message}`, ...logArgs.args ?? []);
}
}
function outputDebugInfoForMessagePublished<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg, TSatisfyArg>(
logging: LoggingService,
message: TMessage): void {
logging?.log(() => { return { message: `${message.constructor.name}`, args: [message] }; });
}
function messagingPublish<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg, TSatisfyArg>(
message: TMessage,
logging: LoggingService
): void {
// 4. BAD: The message passed to generic method doesn't infer types properly.
// ts(2345) on 'message' argument: Type 'AppMessage<TMsg, TSatisfyArg>' is not assignable to type 'AppMessage<unknown, unknown>'.
outputDebugInfoForMessagePublished(logging, message);
}
class MessagingService {
private logging: LoggingService = new LoggingService();
public publishForSatisfy<TMessage extends AppMessage<TMsg, TSatisfyArg>, TMsg, TSatisfyArg>(
message: TMessage,
satisfy: (msg: TMsg, satisfyArg: TSatisfyArg) => void
): void {
message.satisfy = satisfy;
// 3. BAD: The message passed to generic method doesn't infer types properly.
// ts(2345) on 'message' argument: Type 'AppMessage<TMsg, TSatisfyArg>' is not assignable to type 'AppMessage<unknown, unknown>'.
messagingPublish(message, this.logging);
}
}
class TestClass {
messaging: MessagingService = new MessagingService();
testMethod(): void {
// 1. GOOD: The NameAgeMessage constructor argument is strongly-typed.
const message = new NameAgeMessage({ id: 123 });
// 2. BAD: The message passed to generic method doesn't infer types properly.
// ts(2345) on 'message' argument: Argument of type 'NameAgeMessage' is not assignable to parameter of type 'AppMessage<unknown, unknown>'.
this.messaging.publishForSatisfy(message, (msg, satisfyArg) => {
// 5. BAD: Both received arguments are 'unknown'.
// msg is 'unknown'. Should be type NameAgeMsg (from NameAgeMessage)
// satisfyArg is 'unknown'. Should be type NameAgeSatisfyArg (from NameAgeMessage)
});
}
}
答案1
得分: 0
I've replaced the type parameters TMsg
with M
, TSatisfyArg
with S
, and TMessage
with A
(for AppMessage
) to conform to the TypeScript naming convention with which I'm most familiar.
当你调用一个通用函数并且没有手动指定通用类型参数时,编译器会推断这些类型参数。它通过查看函数参数的类型以及函数返回值的类型来进行推断,如果存在这样的上下文的话,还会考虑函数的返回值。
问题出在这个调用签名上:
declare function outputDebugInfoForMessagePublished<A extends AppMessage<M, S>, M, S>(
logging: LoggingService, message: A): void;
在这里,尽管 A
类型参数可以从传递给 message
的参数中推断出来,但编译器无法为类型参数 M
或 S
推断类型,因为在函数参数类型或返回值类型中都没有提及它们。因此,推断将失败,并且它们将回退到 unknown
类型。
function messagingPublish<A extends AppMessage<M, S>, M, S>(
message: A, logging: LoggingService): void {
outputDebugInfoForMessagePublished(logging, message); // error!
// <AppMessage<unknown, unknown>, unknown, unknown>
}
类型参数在约束 A extends AppMessage<M, S>
中是被提及的。但是约束不用于类型推断。在microsoft/TypeScript#7234有一个建议支持这样的推断,但从未实现。相反,推荐的解决方法是在使用处使用交叉类型来替换或补充约束。在上面的情况下,看起来像这样:
declare function outputDebugInfoForMessagePublished<A extends AppMessage<M, S>, M, S>(
logging: LoggingService, message: A & AppMessage<M, S>): void;
当你调用它时,事情突然开始正常推断:
function messagingPublish<A extends AppMessage<M, S>, M, S>(
message: A & AppMessage<M, S>, logging: LoggingService): void {
outputDebugInfoForMessagePublished(logging, message); // okay
// <A, M, S>
}
因此,你只需要将对 A
的引用替换为 A & AppMessage<M, S>
。
但是,我们可以更进一步,至少根据示例代码来看。似乎你实际上并不关心 A
代表的 AppMessage<M, S>
的具体子类型。如果你只关心 M
和 S
,那么我们可以完全忽略 A
,并将 A
替换为 AppMessage<M, S>
(而不是 A & AppMessage<M, S>
)。
这给我们带来了以下代码:
declare function outputDebugInfoForMessagePublished<M, S>(
logging: LoggingService, message: AppMessage<M, S>): void;
function messagingPublish<M, S>(
message: AppMessage<M, S>, logging: LoggingService): void {
outputDebugInfoForMessagePublished(logging, message); // okay
// <M, S>
}
这仍然可以正常推断,但明显简单得多。
英文:
Aside: I've replaced the type parameters TMsg
with M
, TSatisfyArg
with S
, and TMessage
with A
(for AppMessage
) to conform to the TypeScript naming convention with which I'm most familiar (see https://stackoverflow.com/q/66325117/2887218 for more information).
When you call a generic function and do not manually specify generic type arguments, the compiler will infer these type arguments. It does so by consulting the types of the function arguments, and by consulting the expected return type of the function's return value contextually, if such a context exists.
The problem with a call signature like
declare function outputDebugInfoForMessagePublished<A extends AppMessage<M, S>, M, S>(
logging: LoggingService, message: A): void;
is that while the A
type parameter can be inferred from the argument passed in for message
, there are no inference sites for the compiler to infer type arguments for the type parameters M
or S
. These are not mentioned in either the function parameter types or its return type. So inference will definitely fail and they will fall back to unknown
.
function messagingPublish<A extends AppMessage<M, S>, M, S>(
message: A, logging: LoggingService): void {
outputDebugInfoForMessagePublished(logging, message); // error!
// <AppMessage<unknown, unknown>, unknown, unknown>
}
The type parameters are mentioned in the constraint A extends AppMessage<M, S>
. But constraints are not used for type inference. Instead, There was a suggestion at microsoft/TypeScript#7234 to support such inference, but it was never implemented. Instead, the recommended workaround is to replace or supplement the constraint with an intersection at the usage site. In the case above that looks like:
declare function outputDebugInfoForMessagePublished<A extends AppMessage<M, S>, M, S>(
logging: LoggingService, message: A & AppMessage<M, S>): void;
And when you call it, things suddenly start inferring properly:
function messagingPublish<A extends AppMessage<M, S>, M, S>(
message: A & AppMessage<M, S>, logging: LoggingService): void {
outputDebugInfoForMessagePublished(logging, message); // okay
// <A, M, S>
}
So essentially all you need to do is replace references to A
with A & AppMessage<M, S>
.
But we can go further here, at least given the example code. It doesn't look like you actually do anything that cares about the specific subtype of AppMessage<M, S>
that A
represents. If all you care about is M
and S
, then we can forget about A
entirely, and replace A
with just AppMessage<M, S>
(instead of A & AppMessage<M, S>
.
That gives us:
declare function outputDebugInfoForMessagePublished<M, S>(
logging: LoggingService, message: AppMessage<M, S>): void;
function messagingPublish<M, S>(
message: AppMessage<M, S>, logging: LoggingService): void {
outputDebugInfoForMessagePublished(logging, message); // okay
// <M, S>
}
Which still infers properly but is significantly less complicated.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论