2

Consider the two following type definitions (which I believe are equivalent):

type ChangePropertyType1<T, TKeys extends keyof T, TNew> =
  Omit<T, TKeys> & { [K in TKeys]: TNew }

type ChangePropertyType2<T, TKeys extends keyof T, TNew> =
  { [K in keyof T]: K extends TKeys ? TNew: T[K] }

Both work, but the second one gives more useful type information in my IDE—it seems to go one level deeper.

But, the following type declaration seems to require the Omit style:

type RequireProperties<T, TKeys extends keyof T> =
  Omit<T, TKeys> & Required<Pick<T, TKeys>>

Is there some way to rewrite this so that IDE hinting doesn't give less useful type information?

Here's an example of what "less useful type information" means when type information is displayed in IntelliJ/WebStorm. Consider these types:

interface Person {
  name: string,
  age: number
}

type Person1 = ChangePropertyType1<Person, 'age', string>
type Person2 = ChangePropertyType2<Person, 'age', string>

let person1: Person1
let person2: Person2

Hovering over Person1 gives:

Alias for: ChangePropertyType1<Person, "age", string>
Initial type: Omit<Person, "age"> & {age: string}

But hovering over Person2 gives the much more helpful:

Alias for: ChangePropertyType2<Person, "age", string> 
Initial type: {name: Person["name"], age: string}

Similar results for person1 vs. person2 (let person1: Person1, let person2: ChangePropertyType2<Person, "age", string>, which is still not great, but does give one level deeper of type information)

Ideally, the type hint would (also?) show the final "computed type":

{
   name: string
   age: string
}
ErikE
  • 48,881
  • 23
  • 151
  • 196

1 Answers1

4

These types are not equivalent; the second one is a homomorphic mapped type since its key set is keyof T, whereas the first one is not homomorphic because its key set is TKey, which is not of the form keyof Something.

The difference is evident when T has optional and/or readonly properties: ChangePropertyType2 preserves the readonly and ? status of each property, whereas ChangePropertyType1 does not.

type Foo = {a: number; readonly b: number; c?: number;}

// type FooChanged1 = Omit<Foo, keyof Foo> & {a: string; b: string; c: string;}
type FooChanged1 = ChangePropertyType1<Foo, keyof Foo, string>

// type FooChanged2 = {a: string; readonly b: string; c?: string | undefined;}
type FooChanged2 = ChangePropertyType2<Foo, keyof Foo, string>

So which one you should prefer depends on which of these behaviours you want. If you want the behaviour of ChangePropertyType1 but you don't like the way it is shown in the IDE, you can use the Util_FlatType utility type from this answer:

type ChangePropertyType3<T, TKey extends keyof T, TNew> =
  Util_FlatType<Omit<T, TKey> & { [K in TKey]: TNew }>

// type FooChanged3 = {a: string; b: string; c: string;}
type FooChanged3 = ChangePropertyType3<Foo, keyof Foo, string>

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • This is great. Thank you! It makes perfect sense. I have to apologize because after submitting my question I realized it was not as focused as it could be, and I tweaked it a bit. – ErikE Mar 07 '22 at 21:00
  • With your answer I was able to get a few things done in TypeScript that were puzzling me before. The preservation of type optionality and readonly flags is very useful to know. – ErikE Mar 07 '22 at 21:24