-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
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 }