Skip to content

meta()

Creates custom metadata types for cross-cutting concerns like persistence, validation, and devtools.

Signature

ts
// 1. Boolean flag meta - myMeta() returns true
function meta(): MetaType<[], true>;

// 2. Typed value meta - myMeta(value) returns value
function meta<TValue>(): MetaType<[value: TValue], TValue>;

// 3. Custom builder meta - myMeta(...args) returns builder result
function meta<TValue, TArgs extends any[]>(
  builder: (...args: TArgs) => TValue
): MetaType<TArgs, TValue>;

Basic Example

ts
import { meta } from "storion";

// Flag-style meta (value is true)
const persist = meta();

// Meta with custom value
const validate = meta<string>();
const deprecated = meta<{ message: string; since: string }>();

Using Meta

Store-Level Meta

ts
// Single meta
const userStore = store({
  name: 'user',
  state: { name: '', email: '' },
  meta: persist(),       // value: true
  setup: /* ... */,
});

// Multiple metas - use meta.of()
const userStore = store({
  name: 'user',
  state: { name: '', email: '' },
  meta: meta.of(
    persist(),           // value: true
    validate('schema'),  // value: 'schema'
  ),
  setup: /* ... */,
});

Field-Level Meta

ts
// Single field meta
const userStore = store({
  name: 'user',
  state: { name: '', email: '', password: '' },
  meta: persist.for('name'),  // persist name field only
  setup: /* ... */,
});

// Multiple field metas - use meta.of()
const userStore = store({
  name: 'user',
  state: { name: '', email: '', password: '' },
  meta: meta.of(
    persist.for('name'),           // persist name field
    persist.for('email'),          // persist email field
    validate.for('email', 'email'), // validate email field with 'email' rule
    // password not marked - won't be persisted
  ),
  setup: /* ... */,
});

Multiple Fields

ts
meta: meta.of(
  persist.for(["name", "email", "preferences"]),
  validate.for(["email", "phone"], "required"),
);

Naming Conventions

Follow these conventions for clear, consistent meta type names:

CategoryPatternExamples
Boolean flagsAdjective/past participlepersisted, deprecated, hidden, cached
Exclusionsnot + adjectivenotPersisted, notLogged, notCached
Storage targetsin + storageinSession, inLocal, inCloud, inIndexedDB
Validationverb/nounvalidate, minLength, pattern, sanitize
Config valuesnounpriority, debounce, throttle, ttl
Featureswith + feature or -ablewithDevtools, withHistory, loggable
ts
// ✅ Good naming
const persisted = meta(); // Boolean flag
const notPersisted = meta(); // Exclusion
const inSession = meta(); // Storage target
const inLocal = meta(); // Storage target
const validate = meta<string>(); // Validation rule
const priority = meta<number>(); // Config value
const withDevtools = meta(); // Feature flag

// ❌ Avoid unclear names
const p = meta(); // Too short
const persistMeta = meta(); // Redundant "Meta" suffix
const PERSIST = meta(); // Not camelCase
const sessionStore = meta(); // Conflicts with store naming

Querying Meta

In Middleware

ts
function myMiddleware(): StoreMiddleware {
  return (ctx) => {
    const instance = ctx.next();

    // Query meta using the context
    const persistInfo = ctx.meta(persist);

    // Store-level value
    if (persistInfo.store) {
      console.log("Store should be persisted");
    }

    // Field-level values
    for (const [field, value] of Object.entries(persistInfo.fields)) {
      console.log(`Field ${field} has persist value:`, value);
    }

    return instance;
  };
}

From Store Instance

ts
// Get store instance
const instance = container.get(userStore);

// Query meta - returns MetaInfo with store and fields
const validateInfo = instance.meta(validate);

// Store-level value
validateInfo.store;
// Returns: string | undefined

// Field-level values
validateInfo.fields.email;
// Returns: 'email' | undefined

validateInfo.fields.phone;
// Returns: 'required' | undefined

// Iterate all fields
for (const [field, rule] of Object.entries(validateInfo.fields)) {
  console.log(`${field}: ${rule}`);
}

MetaType API

ts
interface MetaType<TValue> {
  // Store-level meta
  (): MetaEntry<any, TValue>;
  (value: TValue): MetaEntry<any, TValue>;

  // Field-level meta
  for<TField extends string>(field: TField): MetaEntry<TField, true>;
  for<TField extends string>(
    field: TField,
    value: TValue
  ): MetaEntry<TField, TValue>;
  for<TField extends string>(fields: TField[]): MetaEntry<TField, true>;
  for<TField extends string>(
    fields: TField[],
    value: TValue
  ): MetaEntry<TField, TValue>;
}

MetaQuery API

The MetaQuery interface provides methods to query metadata:

ts
interface MetaQuery {
  // Default call: returns first matching value for store and each field
  <TValue>(type: MetaType<TValue>): MetaInfo<TValue>;

  // Get all matching values as arrays (when same meta applied multiple times)
  all<TValue>(type: MetaType<TValue>): AllMetaInfo<TValue>;

  // Check if any of the types exist
  any(...types: MetaType<any>[]): boolean;

  // Get field names with a specific meta type
  fields<TValue>(
    type: MetaType<TValue>,
    predicate?: (value: TValue) => boolean
  ): string[];
}

interface MetaInfo<TValue> {
  store: TValue | undefined; // Store-level value
  fields: Record<string, TValue | undefined>; // Field-level values
}

interface AllMetaInfo<TValue> {
  store: TValue[]; // All store-level values
  fields: Record<string, TValue[]>; // All field-level values
}

fields() Method

Get all field names that have a specific meta type. Useful for multi-storage patterns:

ts
const inSession = meta();
const inLocal = meta();

const authStore = store({
  name: "auth",
  state: { token: "", refreshToken: "", userId: "" },
  meta: meta.of(
    inSession.for(["token"]),
    inLocal.for(["refreshToken", "userId"]),
  ),
});

// In middleware
const sessionFields = ctx.meta.fields(inSession);
// Returns: ['token']

const localFields = ctx.meta.fields(inLocal);
// Returns: ['refreshToken', 'userId']

// With predicate filter
const priority = meta<number>();
const highPriorityFields = ctx.meta.fields(priority, (v) => v > 5);

Real-World Examples

Persistence Meta

ts
const persist = meta();

// Single meta usage
meta: persist(); // persist entire store

// Multiple metas
meta: meta.of(
  persist(),                    // persist entire store
  persist.for("settings"),      // also mark specific field
);

// In middleware
const info = ctx.meta(persist);
if (info.store || Object.keys(info.fields).length > 0) {
  // Setup persistence
}

Validation Meta

ts
type ValidationRule = "required" | "email" | "min:N" | "max:N";
const validate = meta<ValidationRule>();

// Usage
meta: meta.of(
  validate.for("email", "email"),
  validate.for("name", "required"),
  validate.for("password", "min:8"),
);

// Query
const rules = userStore.meta(validate).all();
// { email: 'email', name: 'required', password: 'min:8' }

Devtools Meta

ts
const devtools = meta<{ hidden?: boolean; label?: string }>();

// Usage
meta: meta.of(
  devtools({ label: "User Profile" }),
  devtools.for("_internal", { hidden: true }),
);

Deprecated Fields

ts
const deprecated = meta<{ message: string; since: string }>();

// Usage
meta: deprecated.for("oldField", {
  message: "Use newField instead",
  since: "2.0.0",
});

// In middleware - warn when accessing deprecated fields
const deprecatedInfo = ctx.meta(deprecated);
for (const [field, info] of Object.entries(deprecatedInfo.fields)) {
  console.warn(`${field} is deprecated since ${info.since}: ${info.message}`);
}

Factory Meta

Attach meta to service factories:

ts
import { withMeta } from "storion";

// Single meta
const apiService = withMeta(
  (resolver) => ({
    fetch: (url: string) => fetch(url).then((r) => r.json()),
  }),
  persist()
);

// Multiple metas
const authService = withMeta(
  (resolver) => ({ /* ... */ }),
  meta.of(persist(), priority(1))
);

// Query in middleware
const info = ctx.meta(persist);

See Also

Released under the MIT License.