1

**update: made a minimal reproducible example and figured out that it is a pure typescript issue **

I want to have exact types of an object in my application. I created a helper generic type as I found out that it is not nativly supported by typescript.

The issue is however that only when i create the object in the variable it gives the error, but when I insert it as variable as the value it does not give the error.

I have created a playground. This is my code:

    type Exact<T, Struct> = T extends Struct ? (Exclude<keyof T, keyof Struct> extends never ? T : never) : never;

export type MultipleObjectSuccessResponse<T extends Record<string, any>> = {
    success: true
    code: 200;
    data: Array<Exact<T, T>>;
    pagination: {
        page: number;
        per_page: number;
        total_pages: number;
        total_records: number;
    };
};

type MyData = {
    prop1: string;
    prop2: number;
};


const test:MultipleObjectSuccessResponse<MyData> = {
    code: 200,
    success: true,
    data: [{
            prop1: "hello", 
            prop2: 123,
            //TS gives an error for prop3:
            //Object literal may only specify known properties, but 'prop3' does not exist in type 'MyData'. Did you mean to write 'prop1'?(2561)
            prop3: "this will cause an error", 
        }
        
    ],
    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:MultipleObjectSuccessResponse<MyData> = {
    code: 200,
    success: true,
    //Typescript gives no error here
    data: tooManyArray,
    pagination: {
        page: 1,
        per_page: 10,
        total_pages: 1,
        total_records: 1,
    },
}

const test3:MultipleObjectSuccessResponse<MyData> = {
    code: 200,
    success: true,
    //Typescript gives no error here as well
    data: [tooManyObject],
    pagination: {
        page: 1,
        per_page: 10,
        total_pages: 1,
        total_records: 1,
    },
}
5
  • Welcome to Stack Overflow. Please edit the code here to be a minimal reproducible example that demonstrates your issue when we copy and paste it into our own IDEs. There shouldn't be any dependency on private or third party code unless your function clearly lays out that dependency. Right now most of this code gives me errors. Some of it is presumably due to express, but not all of it, right? Is all that z stuff zod? Could you make this a standalone example? Commented Apr 7, 2024 at 22:59
  • Ahh okay thanks! I've updated the message with new code and a playground link where you can see the issue Commented Apr 8, 2024 at 11:03
  • The issue is that, as you said, exact types aren't supported. The closest you can get is to use generics, but that requires things to be generic. The type MultipleObjectSuccessResponse<MyData> isn't generic itself (MultipleObjectSuccessResponse is 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? Commented Apr 8, 2024 at 12:46
  • Yes, that fully addresses the question. I will look into typescript helper functions as the workings to me are not very clear. But it works as I expect it too. Thanks a lot! Commented Apr 8, 2024 at 13:28
  • I will write an answer when I get a chance. Commented Apr 8, 2024 at 13:35

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.