TypeScript doesn't support so-called exact types (the terminology comes from Flow) where excess properties are prohibited. There is a longstanding feature request for it at microsoft/TypeScript#12936 but it has never been implemented.
Therefore no specific type corresponds to Exact<MyData>. The closest you can get is to introduce a generic type like Exact<T, MyData> where T will be checked to see if it has any extra properties compared to Data. But this means you'll need everything to be generic that was specific before. You can't escape the generic and write Exact<MyData, MyData>; that will always just be MyData. Instead you need to carry that T around. And even this will only work in cases where nobody throws away information about extra properties. There is no way, even in principle, to stop this:
const tooManyObject = {
prop1: "hello",
prop2: 123,
prop3: "this will cause an error",
}
const myData: MyData = tooManyObject; // this will always succeed
const myResponse = {
code: 200,
success: true,
data: [myData],
pagination: {
page: 1,
per_page: 10,
total_pages: 1,
total_records: 1,
},
}
You can define MultipleObjectSuccessResponse however you'd like, but there's no way to have TypeScript distinguish between a "good" value and myResponse above. If that's a dealbreaker for you, then what you want is impossible because of the fundamental lack of support for exact types. In this case you should give up with worrying about the issue in TypeScript and just write runtime checks that give you runtime errors if there are too many properties. Or, even better, write your code so that extra properties don't actually hurt anything.
Anyway, that caveat aside, the closest you can get is
type MultipleObjectSuccessResponse<T extends U, U extends object> = {
success: true
code: 200;
data: Array<Exact<T, U>>;
pagination: {
page: number;
per_page: number;
total_pages: number;
total_records: number;
};
};
and then you'd have to fill out both T and U when writing your value:
const test: MultipleObjectSuccessResponse<??, MyData> = { ⋯ };
The U is easy, that's MyData, but what would you plug in for T? It would have to be the type of the input, which means you'd be redundantly describing the extra properties multiple times.
It would be wonderful if you could do something like
const test: MultipleObjectSuccessResponse<infer, MyData> = { ⋯ };
but that is not supported. See Typescript generics, infer object property type without a function call
The only way to get TS to infer a generic type argument is to call a generic function. So we can write a helper function to make up for the missing feature above. Possibly like this:
// helper function
const multipleObjectSuccessResponse = <U extends object,>() => <T extends U,>(
x: MultipleObjectSuccessResponse<T, U>
) => x;
This function takes a type argument, like multipleObjectSuccessResponse<MyData>(), and returns another function which will be used for inference:
const multipleObjectSuccessResponseMyData = multipleObjectSuccessResponse<MyData>();
And now you call that as follows:
const test = multipleObjectSuccessResponseMyData({
code: 200,
success: true,
data: [{
prop1: "hello",
prop2: 123,
}],
pagination: {
page: 1,
per_page: 10,
total_pages: 1,
total_records: 1,
},
}); // okay
That works, and produces a test of type MultipleObjectSuccessResponse<MyData, MyData>. Note that const test = multipleObjectSuccessResponseMyData({⋯}) isn't very different from const test: MultipleObjectSuccessResponse<MyData> = {⋯}, or at least they're close enough that you might be able to see the former in the latter. Now let's see what happens when you make mistakes:
const test = multipleObjectSuccessResponseMyData({
code: 200,
success: true,
data: [{
prop1: "hello",
prop2: 123,
prop3: "this will cause an error", // error, hereabouts
}],
pagination: {
page: 1,
per_page: 10,
total_pages: 1,
total_records: 1,
},
});
const tooManyArray = [{
prop1: "hello",
prop2: 123,
prop3: "this will cause an error",
}]
const tooManyObject = {
prop1: "hello",
prop2: 123,
prop3: "this will cause an error",
}
const test2 = multipleObjectSuccessResponseMyData({
code: 200,
success: true,
data: tooManyArray, // error!
pagination: {
page: 1,
per_page: 10,
total_pages: 1,
total_records: 1,
},
});
const test3 = multipleObjectSuccessResponseMyData({
code: 200,
success: true,
data: [tooManyObject], // error!
pagination: {
page: 1,
per_page: 10,
total_pages: 1,
total_records: 1,
},
});
You get all the expected errors (although the exact wording and location of the error could probably be improved. I'd use a different implementation of Exact<T, U> than yours, but that's out of scope here).
Playground link to code
zstuff zod? Could you make this a standalone example?MultipleObjectSuccessResponse<MyData>isn't generic itself (MultipleObjectSuccessResponseis generic; once you specify a type argument for it the result is just some specific type). You could refactor to use a generic helper function like this playground link shows. Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing?