TypeScript Version:
3.5, 4.0.0-beta
Search Terms:
mapped object type, readonly property, exclude, lookup type
Code
type MyOmit<T, K extends keyof T> = {
[S in Exclude<keyof T, K>]: T[S]
}
declare const foo: MyOmit<{ readonly a: number, b: number }, 'b'>
foo.a = 1 // expects error but none
declare const foo2: Omit<{ readonly a: number, b: number }, 'b'>
foo2.a = 1 // error as expected
Expected behavior:
Assignment to foo.a is an error.
Actual behavior:
No typing error reported.
Playground Link:
https://www.typescriptlang.org/play/index.html?ts=4.0.0-dev.20200729#code/C4TwDgpgBAsiDyBbAlsAPAFQDRQNJQgA9gIA7AEwGcoBrCEAewDMoMA+KAXigG8AoKFADaAZSjJSUAKKEAxgBsAruQho6jFtjxsAugC5WonXwC+fFQoCGAJ2iyGpSsChMGDA3CSo0PKLcvkDvIgUJYGpIqIAEYQ1jhR4ZEx1lAmOADkUelsfK4MAHSWXFAAjIIA9OUEhJCywNSx1gwpUYrOpA4QfOYQVrZQ9o7OeQBMBl7ovv6BpMGhidGx8QvJqRlZOaOFxWVQlQTWTSmWDTW9JORAA
Related Issues:
This is working as intended.
MyOmit is not a homomorphic mapped type and therefore does not copy modifiers from anything. The type Exclude<keyof T, K> is not seen by the compiler as being the keys of some type from which modifiers could be copied.
Omit from the standard library is written the way it is specifically to maintain the connection between T and the keys of the output type in a way that the compiler can see as homomorphic... by using Pick. The closest I can come to your definition that works is to declare a new type parameter that is constrained to keyof T, like this:
type MyOmit<T, K extends keyof T, X extends keyof T = Exclude<keyof T, K>> = {
[S in X]: T[S]
};
You might be getting tripped up by #34793, since the IntelliSense for Omit expands it to a form like your MyOmit which doesn't actually work if you copied it verbatim.
https://www.typescriptlang.org/play/?target=2&module=4&ts=4.0.0-beta#code/C4TwDgpgBA8gPAFQDRQNJQgD2BAdgEwGcoBrCEAewDMoEA+KAXigAUBLAYxMRQFFMOAGwCu+CHDKUayNHToBYAFBKxQgIYAnaBwq5CwKFQoUAXLDgBvKFrX5dgkFDVncwgLYAjCBpQeX7rw0oAF8UAHIPMIVFIwoAOjUmKABGKCgAenSMTEgOYGJvDQogj2EDXF0IJRUIdS0oHT0DWIAmM3grGztcByd-T29ffsCQ8Mjo1oSk1IyswuKnApzanHwgA
Seems work as your expected (If Pick Exclude).
@jcalz Thank you for your clear explanation. But I still cant understand @Kingwl 's working example using this homomorphic mapped type theory. How can Pick establish a connection between T and the keys of the output type(i.e. Exclude <keyof T, K>)?
As far as I know, there are two ways to get a homomorphic mapped type. The normal way is to explicitly write in keyof T where T is a type parameter:
type Homomorphic<T> = {[P in keyof T]: string};
The less common way is to write in K where K is a type parameter constrained to keyof T where T is a type parameter, which gives you a "partially" homomorphic type (see #12826), which is what makes Pick's behavior possible:
type PartiallyHomomorphic<T, K extends keyof T> = {[P in K]: string};
@jcalz Well, so in the Pick case TS sees the constraint of second type parameter and makes a homomorphic mapped type, regardless of what the type argument is? Thanks again for your patient and thorough explanation! Issue closed.
Most helpful comment
As far as I know, there are two ways to get a homomorphic mapped type. The normal way is to explicitly write
in keyof TwhereTis a type parameter:The less common way is to write
in KwhereKis a type parameter constrained tokeyof TwhereTis a type parameter, which gives you a "partially" homomorphic type (see #12826), which is what makesPick's behavior possible: