diff --git a/src/index.spec.ts b/src/index.spec.ts index c2285a9..2306270 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -90,6 +90,19 @@ describe('merged', () => { expect(T.foo(input).conflict).toBe('foo'); }); }); + + describe('can define an interface tag', () => { + it('generate a type without an intersection', () => { + interface Foo { + tag: 'foo'; + x: number; + } + const T = unionize({ + foo: ofType(), + }); + expect(T.foo({ x: 42 }).tag).toBe('foo'); + }); + }); }); describe('separate', () => { diff --git a/src/index.ts b/src/index.ts index a9549ed..40e0ff8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export type Unionized = { +export type Unionized = { _Tags: keyof TaggedRecord; _Record: Record; _Union: TaggedRecord[keyof TaggedRecord]; @@ -6,16 +6,16 @@ export type Unionized = { as: Casts; match: Match; transform: Transform; -} & Creators; +} & Creators; -export type TagsOf> = U['_Tags']; -export type RecordOf> = U['_Record']; -export type UnionOf> = U['_Union']; +export type TagsOf> = U['_Tags']; +export type RecordOf> = U['_Record']; +export type UnionOf> = U['_Union']; -export type Creators = { - [T in keyof Record]: {} extends Record[T] +export type Creators = { + [T in keyof Record]: {} extends UnTagged ? ((value?: {}) => TaggedRecord[keyof TaggedRecord]) - : ((value: Record[T]) => TaggedRecord[keyof TaggedRecord]) + : ((value: UnTagged) => TaggedRecord[keyof TaggedRecord]) }; export type Predicates = { @@ -32,24 +32,31 @@ export type MatchCases = | Cases & NoDefaultProp | Partial> & { default: (variant: Union) => A }; -export type Match = { +export interface Match { (cases: MatchCases): (variant: Union) => A; (variant: Union, cases: MatchCases): A; -}; +} export type TransformCases = Partial< { [T in keyof Record]: (value: Record[T]) => Union } >; -export type Transform = { +export interface Transform { (cases: TransformCases): (variant: Union) => Union; (variant: Union, cases: TransformCases): Union; -}; +} export type MultiValueVariants = { - [T in keyof Record]: { [_ in TagProp]: T } & Record[T] + [T in keyof Record]: Record[T] extends { [_ in TagProp]: T } // does record already has tag with correct value? + ? Record[T] // yes: return as is + : { [_ in TagProp]: T } & Record[T] // no: decorate with tag }; +export type UnTagged = Pick< + Record, + { [k in keyof Record]: k extends TagProp ? never : k }[keyof Record] +>; + export type SingleValueVariants< Record extends SingleValueRec, TagProp extends string, @@ -57,7 +64,9 @@ export type SingleValueVariants< > = { [T in keyof Record]: { [_ in TagProp]: T } & { [_ in ValProp]: Record[T] } }; // Forbid usage of default property; reserved for pattern matching. -export type NoDefaultProp = { default?: never }; +export interface NoDefaultProp { + default?: never; +} export type SingleValueRec = NoDefaultRec<{} | null>; export type MultiValueRec = NoDefaultRec<{ [tag: string]: any }>; @@ -84,15 +93,15 @@ export function unionize< >( record: Record, config: { value: ValProp; tag?: TagProp }, -): Unionized>; +): Unionized, TagProp>; export function unionize( record: Record, config?: { tag: TagProp }, -): Unionized>; +): Unionized, TagProp>; export function unionize(record: Record, config?: { value?: string; tag?: string }) { const { value: valProp = undefined, tag: tagProp = 'tag' } = config || {}; - const creators = {} as Creators; + const creators = {} as Creators; for (const tag in record) { creators[tag] = ((value = {}) => valProp ? { [tagProp]: tag, [valProp]: value } : { ...value, [tagProp]: tag }) as any;