Merge Type
hediet opened this issue ยท 3 comments
Mapped types already enable highly sophisticated scenarios. This proposal discusses a type Merge<T>
that works very well with mapped types. Combining both, even more advanced type operations such as property filters or type subtraction can be implemented without further extending the type system.
The type Merge<T>
is defined by the solution of the following equation for arbitrary property names t1
, ..., tn
and types T1
, ..., TN
:
Merge<{ t1: T1, t2: T2, ..., tN: TN }> = T1 & T2 & ... & TN;
As of TypeScript 2.3, it does not seem possible to find a type Merge
that solves this equation.
Proposed Solution
TypeScript already has a keyof
operator, that evaluates
keyof { a: any, b: any, ... }
to "a" | "b" | ...
.
In accordance with keyof
, this proposal introduces an allkeysof
operator, that evaluates
allkeysof { a: any, b: any, ... }
to "a" & "b" & ..
.
Further, it is proposed to extend index types to support intersection types, so that
{ a: T1, b: T2, ... }["a" & "b" & ...]
evaluates to T1 & T2 & ...
.
Using the allkeysof
operator, a Merge
type that satisfies the equation above can be constructed as following:
type Merge<T> = T[allkeysof T];
Applications
If such a Merge
type exists, the following very useful types can be constructed:
Property Filter
type FilterProps<TFilterProperty extends string, TFilterValue extends string, TType extends any> = Merge<{
[TName in keyof TType]: (
{ [ifConditionMet in TFilterValue]: // matches only for TFilterValue
{ [TName2 in TName]: TType[TName] } // include property when merging
}
&
{ [otherwise: string]: // matches for any string
{ } // add nothing when merging
}
)[TType[TName][TFilterProperty]]
}>;
type Filtered1 = FilterProps<"required", "true", { item1: { required: "false" }, item2: { required: "true" } }>;
// is { item2: { required: "true" } }
type Filtered2 = FilterProps<"required", "false", { item1: { required: "false" }, item2: { required: "true" } }>;
// is { item1: { required: "false" } }
interface String { brand: "string"; }
interface Number { brand: "number"; }
class Foo { brand: "foo" }
type Filtered3 = FilterProps<"brand", "number" | "foo", { item1: string, item2: number, item3: Foo }>;
// is { item2: number, item3: Foo }
Omit
type Omit<TType, TOmitted extends string> =
Merge<
{
[TName in keyof TType]:
(
{ [ifOmitted in TOmitted]:
{ } // contribute nothing when merging
}
&
{ [otherwise: string]: // otherwise
{ [TName2 in TName]: TType[TName] } // contribute property TName when merging
}
)[TName] // test whether TName is omitted or not
}
>;
type Omitted = Omit<{ a: T1, b: T2, c: T3, d: T4 }, "b" | "d">;
// is { a: T1, c: T3 }
Type Subtraction
type Subtract<T1, T2> = Omit<T1, keyof T2>;
type Subtracted = Subtract<{ a: T1, b: T2 }, { a: any }>;
// is { b: T2 }
Property Renaming
type Rename<TPropertyName extends string, TNewPropertyName extends string, TType extends any> = Merge<{
[TName in keyof TType]: (
{ [ifConditionMet in TPropertyName]:
{ [TName2 in TNewPropertyName]: TType[TName] }
}
&
{ [otherwise: string]:
{ [TName2 in TName]: TType[TName] }
}
)[TName]
}>;
type Renamed = Rename<"foo", "bar", { foo: string, baz: number }>;
// is { bar: string, baz: number }
Note that in TS 2.4, Omit can be implemented as follows:
type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
type Test = Omit<{a: number, b: string, c: "hehe"}, "b"|"c">; // {a: number}
type Subtract<T1 extends any, T2> = Omit<T1, keyof T2>;
type Subtracted = Subtract<{ a: string, b: number }, { a: any }>;
Doesn't solve the allkeysof
operator / proposed Merge type though.
copying from #14833, @fightingcat suggests tackling Merge
using union-to-intersection conversion, or alteratively (as per @hediet), per union-to-tuple conversion.
So now there is a solution for the Merge
type:
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never;
type Merge<T> = UnionToIntersection<T[keyof T]>;
๐