Repository navigation
Design Meeting Notes, 3/8/2022 #53169
Description
Using a Homomorphic Mapped Type For Omit
interface Obj {
readonly a: string;
b?: number;
c: boolean;
}
// "homomorphic" - preserves all modifiers in BoxedObj
type Boxify<T> = {
[K in keyof T]: { value: T[K] }
}
type BoxedObj = Boxify<Obj>;
// non-"homomorphic" - loses information for BoxedObj2
type Boxify2<Keys extends keyof Obj> = {
[K in Keys]: { value: Obj[K] }
}
type BoxedObj2 = Boxify2<keyof Obj>;-
Today,
Omitis non-homomorphic. Will not preserve shape.- Why?
keyofon its own does not retain its source type.
-
Today, we can write
Omitwith anasclause withExclude- still considered "homomorphic" (even though at this point "homomorphic" is a misnomer).type Omit<T, DroppedKeys extends PropertyKey> = { [K in keyof T as Exclude<K, DroppedKeys>]: T[K] };
-
Switching
Omitat this point would likely be breaky.- Existing code where
interfaces extends fromOmit
- Existing code where
-
Using a "homomorphic"
Omitmakes code flow work better because it can produce union types. -
Could choose to have
Omit(original) andMappedOmit- orOmit(improved) andLegacyOmit.- Swapping
Omitto the "better" version is breaky - at least one package breaks.
- Swapping
-
We really don't want to have 2
Omits, and want to push on an improvedOmit.- Existing code can write
Pick<T, Omit<keyof T, DroppedKeys>>
- Existing code can write
-
Wait, should
Pickbe fixed to use a homomorphic mapped type too?- OH NOOOOOOOOOOOOOOOOOOOOOOOOOOOO
- Almost had a slam dunk on just fixing
Omit.
-
If you fix
Pick, thenOmitdefined asPick<T, Exclude<keyof T, DroppedKeys>>is also homomorphic, right?- Almost - but
Omitis not going to distribute, when we writeExclude<keyof T, DroppedKeys>
- Almost - but
-
So each of them need to be written as their own mapped types written in terms of
keyof T. -
Note: the
asclause forPickshould use an intersection, notExtract- Why?
- Intersections are much faster for unions of literals.
- What's the difference in the general case?
- For literal types, they're the same.
- For object types and generic types, intersections combine and don't always "eradicate" other types to
never.
- Why?
Activity
Andarist commentedon Mar 8, 2023
Reads like a good novel. It has everything:
juanrgm commentedon Mar 10, 2023
The problem with the "homomorphic" version of
Omitis that we lose the docblock and the goto feature:Example
Andarist commentedon Mar 10, 2023
Hm, I expected that to work in 5.0 since #51650 . It doesn't though... I'll investigate what's happening there
Andarist commentedon Mar 11, 2023
It seems that my fix (#51650) had a bug. Accidentally, my other PR already fixes this but since its fate is still not determined, I will prepare a separate PR fixing this tomorrow.
Andarist commentedon Feb 7, 2024
I was often confused about this in the past too. In reality, mapped types are not split into two groups though (homomorphic and non-homomorphic). They are split into 3: homomorphic, non-homomorphic with non-unknown modifiers type and non-homomorphic with an unknown moifiers type.
Homomorphic mapped types preserve potential arrayness of the input, they distribute over unions and they preserve original modifiers. So in a sense, homomorphic mapped types are a subset of the mapped typed with non-unknown modifiers type.
I might be using this lingo slightly wrong (so don't quote me on that) but that's roughly how things are called under the hood (there is no specific name for a mapped type with a (non-)unknown modifiers type though, it's just something that is checked when "transferring" the modifiers on the output type etc).
jcalz commentedon Feb 7, 2024
Okay, so "non-homomorphic with known modifiers" is specifically what you get when you write
{[P in K]: ⋯}whereK extends keyof Tfor some genericT?Andarist commentedon Feb 7, 2024
Yes, that's one way to end up with this "type".
getModifiersTypeFromMappedTypein the source code is defined as follows:whereas
isMappedTypeHomomorphicis just this:and
getHomomorphicTypeVariableis this:Sorry for quoting a bunch of source code but I feel like it's just the only way to answer this precisely ;p Note that an
IndexTypemeanskeyof Something. It's also useful to inspect mapped type declaration in the https://ts-ast-viewer.com/# to learn more about how specific parts of it might be called (what is its constraint, type parameter, etc)