Home > front end >  Why is the TypeScript Generic Partial<T> ignored here?
Why is the TypeScript Generic Partial<T> ignored here?

Time:12-10

I built a Generic Type for sort options. Now I am left with two alternatives, which both seem right.

// Option 1
export type SortSearchBy<DtoType> = Record<keyof Partial<Omit<DtoType, 'orderByFields'>>, SortBy>
    
// Option 2
export type SortSearchBy2<DtoType> = {
    [x in keyof Partial<Omit<DtoType, 'orderByFields'>>]: SortBy 
}

However the fields of DtoType are only optional in the second option. For the first option, all fields of DtoType (execpt for orderByOption) have to be filled:

class AnimalDto {
    name: string
    age: number
    orderByFields: SortSearchBy<AnimalDto>
}

// here you can('t) set 
orderByOptions = {
   name: {
     option: 'ASC',
     order: 1
   },
   // Error: Type is missing the following properties... (age)
}

class PersonDto {
   name: string
   age: number
   orderByFields: SortSearchBy2<PersonDto> // Notice the 2
}

// while here it is possible
orderByFields = {
   name: {
     option: 'ASC',
     order: 1
   } 
   // No Error
}

Why does this behave differently, when I have used the Partial generic for both types.

CodePudding user response:

Solution

You are probably not looking for keyof Partial<Omit<DtoType, 'orderByFields'>> you probably want:


export type SortSearchBy<DtoType> = Partial<Record<keyof Omit<DtoType, 'orderByFields'>, SortBy>>

Playground Link

Reason

Generally keyof T and keyof Partial<T> should be the same union of key. After all keyof just returns the keys of a type regardless of the optionality of the properties.

While the statement above is generally true, mapped types have a special behavior. If you map over keyof T (ie you have a type like { [P in keyof T]: U }) where T is any object type, the mapped type will turn homomorphic, meaning it will preserve the structure (optionality, readonly modifier) of the type T you are mapping over.

So { [P in keyof Partial<Omit<DtoType, 'orderByFields'>>]: SortBy } will be a homomorphic mapped type and it will preserve the structure of the type you are mapping. This means the properties in the resulting type will be optional since Partial<Omit<DtoType, 'orderByFields'>> has all its properties marked as optional

In Record<keyof Partial<Omit<DtoType, 'orderByFields'>>, SortBy> the mapping happens in Record and it has no knowledge of were the keys it's mapping over came from, as the keyof happens before Record gets a hold of the keys.

CodePudding user response:

In option 1, you are simply extracting the union of keys of the generated record and stating that this union is the accepted keys in your generated record. It doesn't retain their typings (optional, readonly).

In option 2, you are mapping the existing types into a new type. The in operator checks a to see whether x is in the union of keys. If it is, it maps that property and retains all type information such as the type being optional.

  • Related