3

I came across this typescript challenge MyReadonly2. Here is my original solution:

// doesn't work as `Exclude` could potentially change the existing `readonly` modifier in the object 
type MyReadonly2<T, K extends keyof T = keyof T> =  { [P in Exclude<keyof T, K>]: T[P] } & { readonly [P in K]: T[P] }

Here is one of the correct answers:

type MyReadonly2<T, K extends keyof T = keyof T> =  Omit<T, K> & { readonly [P in K]: T[P] }

Basically both approaches have the same idea, but how can I make sure I'm not changing the object's readonly modifier when I copy its members? The only difference from Omit is just the type constraint on K:

type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }

Why does it make a difference?

Janice Zhong
  • 836
  • 7
  • 16
  • Waitwut. I copy `Omit` as `Omit1` and it suddenly *starts losing readonlys* – Dimava Jul 02 '23 at 11:58
  • Yeah, TS has the concept of a *homomorphic* mapped type (which preserves optional/readonly modifiers) and it kicks in only in certain circumstances; either with `{[P in keyof ⋯]: ⋯}` or `{[P in K]: ⋯}` where `K extends keyof ⋯`. If you try to "inline" a homomorphic mapped type you might well break that part of it. See the linked q/a for more info. – jcalz Jul 02 '23 at 16:49

1 Answers1

2

keyof clause in Typescript results in intermediate type with readonly/optional metadata

type X = keyof {a?: 1} // 'a' & {[optional]: true, [readonly]: false}

However, this metadata is lost on any operation with the type

type X = keyof {a?: 1} & string // just 'a'
type X = Extract<keyof {a?: 1}, string> // just 'a'
type X = [keyof {a?: 1}] // just ['a']

so when you { [P in Exclude<keyof T, K>]: T[P] } you lose that metadata when you Exclude<keyof T, ...>
Use { [P in keyof T as P extends K ? never : P>]: T[P] } instead

Also,

type Omit2<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }

is not a correct implementation (even if that's the one TS builtin shows) as it loses readonlys.

https://tsplay.dev/N7z2qw

Dimava
  • 7,654
  • 1
  • 9
  • 24