Skip to content

Commit 2751bad

Browse files
authored
fix: stop or() union constructor from stripping branding (#88)
BREAKING CHANGE: union types built with the `.or()` constructor are now possibly stricter as the branding is not unintentionally stripped anymore.
1 parent e6521cf commit 2751bad

File tree

4 files changed

+30
-8
lines changed

4 files changed

+30
-8
lines changed

etc/types.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
6969
literal(input: DeepUnbranded<ResultType>): ResultType;
7070
maybeStringify(value: ResultType): string | undefined;
7171
abstract readonly name: string;
72-
or<Other>(_other: BaseTypeImpl<Other, any>): Type<ResultType | Other>;
72+
or<Other extends BaseTypeImpl<unknown>>(_other: Other): Type<ResultType | TypeOf<Other>>;
7373
stringify(value: ResultType): string;
7474
abstract readonly typeConfig: TypeConfig;
7575
protected typeParser?(input: unknown, options: ValidationOptions): Result<unknown>;

markdown/types.basetypeimpl.or.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ Union this Type with another Type.
99
**Signature:**
1010

1111
```typescript
12-
or<Other>(_other: BaseTypeImpl<Other, any>): Type<ResultType | Other>;
12+
or<Other extends BaseTypeImpl<unknown>>(_other: Other): Type<ResultType | TypeOf<Other>>;
1313
```
1414
1515
## Parameters
1616
17-
| Parameter | Type | Description |
18-
| --------- | ----------------------------------------------------------------- | ----------- |
19-
| \_other | [BaseTypeImpl](./types.basetypeimpl.md)<!-- -->&lt;Other, any&gt; | |
17+
| Parameter | Type | Description |
18+
| --------- | ----- | ----------- |
19+
| \_other | Other | |
2020
2121
**Returns:**
2222
23-
[Type](./types.type.md)<!-- -->&lt;ResultType \| Other&gt;
23+
[Type](./types.type.md)<!-- -->&lt;ResultType \| [TypeOf](./types.typeof.md)<!-- -->&lt;Other&gt;&gt;
2424
2525
## Remarks
2626

src/base-type.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
Type,
1414
TypeImpl,
1515
TypeLink,
16+
TypeOf,
1617
TypeguardFor,
1718
TypeguardResult,
1819
ValidationOptions,
@@ -457,7 +458,7 @@ export abstract class BaseTypeImpl<ResultType, TypeConfig = unknown> implements
457458
* See {@link UnionType} for more information about unions.
458459
*/
459460
// istanbul ignore next: using ordinary stub instead of module augmentation to lighten the load on the TypeScript compiler
460-
or<Other>(_other: BaseTypeImpl<Other, any>): Type<ResultType | Other> {
461+
or<Other extends BaseTypeImpl<unknown>>(_other: Other): Type<ResultType | TypeOf<Other>> {
461462
throw new Error('stub');
462463
}
463464

src/types/union.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DeepUnbranded, The } from '../interfaces';
2-
import { createExample, defaultUsualSuspects, testTypeImpl } from '../testutils';
2+
import { assignableTo, createExample, defaultUsualSuspects, testTypeImpl } from '../testutils';
33
import { boolean } from './boolean';
44
import { object, partial } from './interface';
55
import { keyof } from './keyof';
@@ -493,3 +493,24 @@ testTypeImpl({
493493
],
494494
],
495495
});
496+
497+
const BrandedA = literal('A').withBrand('BrandA');
498+
const BrandedB = literal('B').withBrand('BrandB');
499+
500+
type BrandedUnion = The<typeof BrandedUnion>;
501+
const BrandedUnion = union('BrandedUnion', [BrandedA, BrandedB]);
502+
503+
type BrandedOr1 = The<typeof BrandedOr1>;
504+
const BrandedOr1 = BrandedA.or(BrandedB);
505+
506+
type BrandedOr2 = The<typeof BrandedOr2>;
507+
const BrandedOr2 = BrandedB.or(BrandedA);
508+
test('equivalence between `union()` and `.or()`', () => {
509+
// Validating issue #87
510+
assignableTo<BrandedUnion>(BrandedOr1.literal('A'));
511+
assignableTo<BrandedUnion>(BrandedOr2.literal('A'));
512+
assignableTo<BrandedOr1>(BrandedUnion.literal('A'));
513+
assignableTo<BrandedOr2>(BrandedUnion.literal('A'));
514+
// @ts-expect-error Unbranded literal not assignable to branded literal union type
515+
assignableTo<BrandedOr1>('B');
516+
});

0 commit comments

Comments
 (0)