prefer-type-fest-tagged-brands
Prefers TypeFest Tagged for branded primitive identifiers over ad-hoc __brand/__tag intersection patterns.
Targeted pattern scopeโ
This rule targets ad-hoc brand-marker intersections and legacy alias names used for branded primitives.
What this rule reportsโ
- Type aliases that use intersection branding with explicit brand-marker fields.
- Type references that resolve to imported
Opaque/Brandedaliases. - Existing
Taggedusage is ignored.
Detection boundariesโ
- โ Reports ad-hoc brand-marker intersections by default.
- โ
Reports imported
Opaque/Brandedaliases by default. - โ Does not report namespace-qualified alias usage.
- โ
Auto-fixes imported legacy alias references to
Taggedwhen replacement is syntactically safe. - โ Does not auto-fix ad-hoc intersection branding declarations.
- โ
Enforcement surface is configurable with
enforceAdHocBrandIntersectionsandenforceLegacyAliases.
Why this rule existsโ
Tagged provides a standard, reusable branded-type approach that improves consistency and readability.
โ Incorrectโ
type UserId = string & { readonly __brand: "UserId" };
โ Correctโ
type UserId = Tagged<string, "UserId">;
Behavior and migration notesโ
Tagged<Base, Tag>standardizes branded identity types.- This rule targets both structural brand fields (
__brand,__tag) and legacy alias references (Opaque,Branded). - Use canonical
Taggedaliases for IDs and domain markers to keep branding semantics consistent across packages.
Optionsโ
This rule accepts a single options object:
type PreferTypeFestTaggedBrandsOptions = {
/**
* Whether to report structural ad-hoc branding intersections.
*
* @default true
*/
enforceAdHocBrandIntersections?: boolean;
/**
* Whether to report imported legacy aliases like Opaque/Branded.
*
* @default true
*/
enforceLegacyAliases?: boolean;
};
Default configuration:
{
enforceAdHocBrandIntersections: true,
enforceLegacyAliases: true,
}
Flat config setup (default behavior):
import typefest from "eslint-plugin-typefest";
export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-tagged-brands": [
"error",
{
enforceAdHocBrandIntersections: true,
enforceLegacyAliases: true,
},
],
},
},
];
enforceAdHocBrandIntersections: falseโ
Ignores structural ad-hoc intersections, while still reporting legacy aliases:
import typefest from "eslint-plugin-typefest";
export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-tagged-brands": [
"error",
{
enforceAdHocBrandIntersections: false,
enforceLegacyAliases: true,
},
],
},
},
];
import type { Opaque } from "type-aliases";
type A = string & { readonly __brand: "UserId" }; // โ
Not reported
type B = Opaque<string, "UserId">; // โ Reported
enforceLegacyAliases: falseโ
Ignores imported Opaque/Branded aliases, while still reporting ad-hoc intersections:
import typefest from "eslint-plugin-typefest";
export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-tagged-brands": [
"error",
{
enforceAdHocBrandIntersections: true,
enforceLegacyAliases: false,
},
],
},
},
];
import type { Opaque } from "type-aliases";
type A = Opaque<string, "UserId">; // โ
Not reported
type B = string & { readonly __brand: "UserId" }; // โ Reported
Additional examplesโ
โ Incorrect โ Additional exampleโ
type OrderId = string & { readonly __tag: "OrderId" };
โ Correct โ Additional exampleโ
type OrderId = Tagged<string, "OrderId">;
โ Correct โ Repository-wide usageโ
type TenantId = Tagged<string, "TenantId">;
ESLint flat config exampleโ
import typefest from "eslint-plugin-typefest";
export default [
{
plugins: { typefest },
rules: {
"typefest/prefer-type-fest-tagged-brands": "error",
},
},
];
When not to use itโ
Disable this rule if existing brand encodings must remain for backward compatibility.
Package documentationโ
TypeFest package documentation:
Source file: source/tagged.d.ts
/**
Attach a "tag" to an arbitrary type. This allows you to create distinct types, that aren't assignable to one another, for distinct concepts in your program that should not be interchangeable, even if their runtime values have the same type. (See examples.)
A type returned by `Tagged` can be passed to `Tagged` again, to create a type with multiple tags.
[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d)
A tag's name is usually a string (and must be a string, number, or symbol), but each application of a tag can also contain an arbitrary type as its "metadata". See {@link GetTagMetadata} for examples and explanation.
A type `A` returned by `Tagged` is assignable to another type `B` returned by `Tagged` if and only if:
- the underlying (untagged) type of `A` is assignable to the underlying type of `B`;
- `A` contains at least all the tags `B` has;
- and the metadata type for each of `A`'s tags is assignable to the metadata type of `B`'s corresponding tag.
There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
- [Microsoft/TypeScript#4895](https://github.com/microsoft/TypeScript/issues/4895)
- [Microsoft/TypeScript#33290](https://github.com/microsoft/TypeScript/pull/33290)
@example
```
import type {Tagged} from 'type-fest';
type AccountNumber = Tagged<number, 'AccountNumber'>;
type AccountBalance = Tagged<number, 'AccountBalance'>;
function createAccountNumber(): AccountNumber {
// As you can see, casting from a `number` (the underlying type being tagged) is allowed.
return 2 as AccountNumber;
}
declare function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance;
// This will compile successfully.
getMoneyForAccount(createAccountNumber());
// But this won't, because it has to be explicitly passed as an `AccountNumber` type!
// Critically, you could not accidentally use an `AccountBalance` as an `AccountNumber`.
// @ts-expect-error
getMoneyForAccount(2);
// You can also use tagged values like their underlying, untagged type.
// I.e., this will compile successfully because an `AccountNumber` can be used as a regular `number`.
// In this sense, the underlying base type is not hidden, which differentiates tagged types from opaque types in other languages.
const accountNumber = createAccountNumber() + 2;
```
@example
```
import type {Tagged} from 'type-fest';
// You can apply multiple tags to a type by using `Tagged` repeatedly.
type Url = Tagged<string, 'URL'>;
type SpecialCacheKey = Tagged<Url, 'SpecialCacheKey'>;
// You can also pass a union of tag names, so this is equivalent to the above, although it doesn't give you the ability to assign distinct metadata to each tag.
type SpecialCacheKey2 = Tagged<string, 'URL' | 'SpecialCacheKey'>;
```
@category Type
*/
Rule catalog ID: R067