I'm trying to create generic message types, with ever-narrower types, the most narrow of-which is actually inferred during the calling of a function or creation of a class.
Take this code:
type MessageHeaderType = 'REQUEST'|'RESPONSE'|'UPDATE'|'ERROR'|'EVENT'
type UnknownRecord = object
interface Message<MessageBody = UnknownRecord> {
header: {
id?: string
type: MessageHeaderType
},
body: MessageBody
}
interface MessageUpdate<MessageBody = MessageUpdateBody> extends Message<MessageBody> {
header: {
id: string
type: 'UPDATE'
}
}
interface MessageUpdateBody {
code: string
data?: UnknownRecord
}
This works nicely and I can create a MessageUpdate
easily:
const ExampleMessageUpdate: MessageUpdate = {
header: {
id: '123',
type: 'UPDATE'
},
body: {
code: 'SOME_UPDATE',
data: {
foo: 'bar'
}
}
} // <== All good!
The problem comes when I allow a user of said code to generate their own message update body "on the fly" (still compile-time, obvs!). The below code does not let me index the generic that is passed in their implementation of a particular MessageUpdate
type:
function createMessage<ThisMessageUpdateBody>(code: string, data?: ThisMessageUpdateBody['data']): MessageUpdate<ThisMessageUpdateBody> {
const message: MessageUpdate<ThisMessageUpdateBody> = {
header: {
id: '123',
type: 'UPDATE'
},
body: {
code,
data
}
}
return message
}
I get the error on the function parameters:
Type '"data"' cannot be used to index type 'ThisMessageUpdateBody'
How can I achieve a further narrowing of this type, in the same manner, without an error?
CodePudding user response:
Update
Here's a slightly changed version to accommodate OP's comment.
If using an object as a single parameter of the function is not an option, then at least I would go with type aliases for keeping it safe and also staying away from string indexes.
type BodyCodeType = string;
type BodyDataType = UnknownRecord;
interface MessageUpdateBody {
code: BodyCodeType;
data?: BodyDataType;
}
interface MessageUpdate<MessageBody = MessageUpdateBody>
extends Message<MessageBody> {
header: {
id: string;
type: 'UPDATE';
};
}
function createMessage<ThisMessageUpdateBody extends MessageUpdateBody>(
code: BodyCodeType,
data: BodyDataType
): MessageUpdate<ThisMessageUpdateBody> {
const message: MessageUpdate<ThisMessageUpdateBody> = {
header: {
id: '123',
type: 'UPDATE',
},
body: {
code,
data,
} as ThisMessageUpdateBody,
};
return message;
}
const customMsg = createMessage('test_code', { foo: 'bar' });
console.log(customMsg); // OK
Btw, you still need the as ThisMessageUpdateBody
cast, otherwise the compiler will get confused on the body
:
Personally I would rely on data inference from the actual template type, and so I would use an object for the function parameter. Like this:
function createMessage<ThisMessageUpdateBody extends MessageUpdateBody>({
code,
data,
}: ThisMessageUpdateBody): MessageUpdate<ThisMessageUpdateBody> {
const message: MessageUpdate<ThisMessageUpdateBody> = {
header: {
id: '123',
type: 'UPDATE',
},
body: {
code,
data,
} as ThisMessageUpdateBody,
};
return message;
}
const customMsg = createMessage({
code: 'test_code',
data: { foo: 'bar' },
});
And not using strings for indexing is another win, because now if you change from data
to payload
inside MessageUpdateBody
, it will throw a clear compile time error.
CodePudding user response:
One way to refer the index of generic type is to create another generic and assign this generic as the type, so in your case it could be something like
function createMessage<ThisMessageUpdateBody extends MessageUpdateBody, Data extends ThisMessageUpdateBody['data']>(code: string, data?: Data): MessageUpdate<ThisMessageUpdateBody> {
const body = {
code,
data
};
const message = {
header: {
id: '123',
type: UPDATE as typeof UPDATE
},
body
}
return message
}
Although above snippets still throws an error because ThisMessageUpdateBody
could be of any sub type of MessageUpdateBody i.e it could contains many other properties apart from code
& data
(below is the error).
Type '{ code: string; data: Data | undefined; }' is not assignable to type 'ThisMessageUpdateBody'.
'{ code: string; data: Data | undefined; }' is assignable to the constraint of type 'ThisMessageUpdateBody', but 'ThisMessageUpdateBody' could be instantiated with a different subtype of constraint 'MessageUpdateBody'.(2322)
Suggestion 1:
createMessage
to have body
parameter instead of code
& data
function createMessage<ThisMessageUpdateBody extends MessageUpdateBody= MessageUpdateBody>(body: ThisMessageUpdateBody): MessageUpdate<ThisMessageUpdateBody> {
const message = {
header: {
id: '123',
type: UPDATE as typeof UPDATE
},
body
}
return message
}
Suggestion 2:
instead of declaring generic type of messageBody createMessage
could declare generic type of body data
. This lets us fulfil the constraint that message body would always have two properties (code: string
& data: CustomData
)
// MessageBody declare generic for "body.data" type
interface MessageUpdateBody<MessageBodyData = UnknownRecord> {
code: string
data?: MessageBodyData
}
function createMessage<MessageBodyData = UnknownRecord>(code: string, data: MessageBodyData): MessageUpdate<MessageUpdateBody<MessageBodyData>> {
const message = {
header: {
id: '123',
type: UPDATE as typeof UPDATE
},
body: {
code,
data
}
}
return message
}