问题
When optional and required property are merged via intersection, required wins
type A = { who: string }
type B = { who?: string }
// $ExpectType {who:string}
type R = A & B
This may lead to runtime errors, when for instance, dealing with default params pattern within a function
type Params = {
who: string
greeting: string
}
const defaults: Params = {
greeting: 'Hello',
who: 'Johny 5',
}
function greeting(params: Partial<Params>){
// $ExpectType Params
const merged = {...defaults, ...params}
return `${merged.greeting.toUpperCase()} ${merged.who} !`
}
// @throws - TypeError: Cannot read property 'toUpperCase' of undefined
greeting({greeting:undefined, who: 'Chuck'})
Question:
as what I described is how TS compiler behaves, question is, how to create mapped type, that would resolve that intersection to union
so something like:
type SafeMerge<A,B>=....
// $ExpectType {greeting?: string | undefined, who?: string | undefined }
type Result = SafeMerge<Params, Partial<Params>>
Example with mixed types:
// $ExpectType {one?: number | undefined, two: string, three: boolean }
type Result = SafeMerge<{one: number, two:string}, {one?: number, three: boolean}>
回答1:
Getting a merged type that is an amalgamation of two types with each property a union of possibilities is simple. We can just use a mapped type, over the keys of both constituent types:
type SafeMerge<T, U> = {
[P in keyof T | keyof U] :
| (T extends Partial<Record<P, any>> ? T[P] : never)
| (U extends Partial<Record<P, any>> ? U[P] : never)
}
type Result = SafeMerge<{one: number, two:string }, {one?: number, three: boolean }>
// Result:
// type Result = {
// one: number | undefined;
// two: string;
// three: boolean;
// }
Play
The problem with the solution above is that we loose the optionality of the keys (also the readonly-ness, which is probably less of a concern for this use case). Homomorphic mapped types keep modifiers, but unfortunately we can't use one here since we don't really fit in any of the patterns for homomorphic mapped types ({ [P in keyof T] : T[P] }
or {[P in K]: T[P] }
where K is a type parameter with K extends keyof T
, see Here and Here).
We can extract the optional keys and use two mapped types, one for any optional keys (keys that are optional in at least one of the constituents) and one for the required keys:
type OptionalPropertyOf<T> = Exclude<{
[K in keyof T]: T extends Record<K, T[K]>
? never
: K
}[keyof T], undefined>
type SafeMerge<T, U> = {
[P in OptionalPropertyOf<T> | OptionalPropertyOf<U>]?:
| (T extends Partial<Record<P, any>> ? T[P] : never)
| (U extends Partial<Record<P, any>> ? U[P] : never)
} & {
[P in Exclude<keyof T | keyof U, OptionalPropertyOf<T> | OptionalPropertyOf<U>>]:
| (T extends Partial<Record<P, any>> ? T[P] : never)
| (U extends Partial<Record<P, any>> ? U[P] : never)
}
type Result = SafeMerge<{one: number, two:string, three: number}, {one?: number, three: boolean}>
// Result:
// type Result = {
// one?: number | undefined;
// } & {
// two: string;
// three: number | boolean;
// }
type Id<T> = {} & { [P in keyof T]: T[P] }
type FlattenedResult = Id<SafeMerge<{one: number, two:string, three: number }, {one?: number, three: boolean}>>
// type FlattenedResult = {
// one?: number | undefined;
// two: string;
// three: number | boolean;
// }
Play
Optionally we can Id
to flatten the intersection as I did in the example above, but that is optional.
来源:https://stackoverflow.com/questions/57474241/typescript-create-union-instead-intersection-when-merging-optional-with-require