microsoft/TypeScript

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]>;

๐Ÿ’ƒ