Reputation: 10218
While building the typesafe version of some library, I've run into the issue involving complex mapped types and got stuck.
Here's the simplified version of this issue. We have two "boxed" types - Strong<T>
and Weak<T>
, which can be thought of as "holding a value" and "possibly holding a value" correspondingly. Then, there is an object containing these "boxes", and I need to write a function which will unify it all into one large Strong
box holding an object, by converting Strong<T>
to required fields of type T
and Weak<T>
to optional fields of type T
. Later, in other part of the code, I'll "unbox" the type Strong<T>
to T
.
The problem is that I'm not able to make the fields conditionally optional. The code in question looks like this:
type Strong<T> = { __phantom?: T, marker: 'strong' };
type Weak<T> = { __phantom?: T, marker: 'weak' };
// helper functions, to be used later
function strong<T>(): Strong<T> {
return { marker: 'strong' };
}
function weak<T>(): Weak<T> {
return { marker: 'weak' };
}
// input type for my function
type Rec = { [index: string]: Strong<unknown> | Weak<unknown> };
// output type for my function; how to define it?
type Unified<T extends Rec> = { __phantom?: T, marker: 'unified' };
// put there something, just so that this type is unique
function unify<T extends Rec>(input: T): Strong<Unified<T>> {
// implementation is irrelevant now
return {} as any;
}
// when we have the Strong value, we can 'unbox' the type
type Unboxed<T> = T extends Strong<infer I> ? I : never;
// ...so, how to make this compile?
const unified = unify({ strong: strong<string>(), weak: weak<string>() });
const valid: Array<Unboxed<typeof unified>> = [{ strong: '' }, { strong: '', weak: '' }];
// ...and at the same time forbid this?
const invalid: Array<Unboxed<typeof unified>> = [{}, {weak: ''}, {unknown: ''}]
One attempt was to take the union of all object values, transforming them and then converting with UnionToIntersection
, but the type was complex enough for type inference to get stuck and spit out a bunch of unknown
s.
How can I define the Unified
type to be properly derived from the input?
Upvotes: 1
Views: 928
Reputation: 10218
The key point was found in the Github issue discussing reverse problem: how to determine whether the fields of the type are required or optional. Here's the solution provided there:
export type OptionalPropertyNames<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never
}[keyof T];
export type RequiredPropertyNames<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K
}[keyof T];
Using similar technique, I was able to solve the problem. First, we need two helper types similar to the above:
type OptionalPropertyNames<T> = {
[K in keyof T]: T[K] extends Weak<any> ? K : never
}[keyof T];
type RequiredPropertyNames<T> = {
[K in keyof T]: T[K] extends Weak<any> ? never : K
}[keyof T];
The key point here is the fact that never
values defined on the object type effectively disappear, when we index this type.
Then, the Unified
type is simply an intersection of two mapped types, each of which is unboxing its own part of values:
type Unified<T extends Rec> = {
[K in OptionalPropertyNames<T>]?: T[K] extends Weak<infer I> ? I : never
} & {
[K in RequiredPropertyNames<T>]: T[K] extends Strong<infer I> ? I : never;
}
Upvotes: 2