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)
});
}
}