Skip to content

Complex Types in Emi

In Emi modules, you can define complexes as reusable types. Each complex requires a class name and a location where it should be imported from. Instead of using primitive types like number or string, you can specify complex: ClassName inside the action field definition.

  • If you prefix the complex with ”+”, like +ClassName, Emi will automatically instantiate the value when parsing the response.
  • Built-in types such as Date can be used directly as complexes.
  • User-defined types such as Money need to be registered in the complexes array of your module.

Here you can see based on defining 2 complex fields, we are generating them as class instances

Example schema:

name: emiComplexFields
complexes:
- name: Money
location: ../some/directory/Money
actions:
- name: sample
url: http://localhost:8081 (for test we use override)
method: post
description: Demo action that shows how complex types can be used in an action
out:
envelope: GResponse
fields:
- name: date
complex: +Date
description: Use javascript Date class as complex type
- name: money
complex: +Money
description: External money type
import { GResponse } from "./sdk/envelopes/index";
import { Money } from "../some/directory/Money";
import { buildUrl } from "./sdk/common/buildUrl";
import {
fetchx,
handleFetchResponse,
type FetchxContext,
type PartialDeep,
type PlainOf,
type TypedRequestInit,
} from "./sdk/common/fetchx";
/**
* Action to communicate with the action sample
*/
export type SampleActionOptions = {
queryKey?: unknown[];
qs?: URLSearchParams;
};
/**
* SampleAction
*/
export class SampleAction {
//
static URL = "http://localhost:8081 (for test we use override)";
static NewUrl = (qs?: URLSearchParams) =>
buildUrl(SampleAction.URL, undefined, qs);
static Method = "POST";
static Fetch$ = async (
qs?: URLSearchParams,
ctx?: FetchxContext | null,
init?: TypedRequestInit<unknown, unknown>,
overrideUrl?: string,
) => {
return fetchx<GResponse<SampleActionRes>, unknown, unknown>(
overrideUrl ?? SampleAction.NewUrl(qs),
{
method: SampleAction.Method,
...(init || {}),
},
ctx,
);
};
static Fetch = async (
init?: TypedRequestInit<unknown, unknown>,
{
creatorFn,
qs,
ctx,
onMessage,
overrideUrl,
}: {
creatorFn?: ((item: unknown) => SampleActionRes) | undefined;
qs?: URLSearchParams;
ctx?: FetchxContext | null;
onMessage?: (ev: MessageEvent) => void;
overrideUrl?: string;
} = {
creatorFn: (item) => new SampleActionRes(item),
},
) => {
creatorFn = creatorFn || ((item) => new SampleActionRes(item));
const res = await SampleAction.Fetch$(qs, ctx, init, overrideUrl);
return handleFetchResponse(
res,
(data) => {
const resp = new GResponse<SampleActionRes>();
if (creatorFn) {
resp.setCreator(creatorFn);
}
resp.inject(data);
return resp;
},
onMessage,
init?.signal,
);
};
static Definition = {
name: "sample",
url: "http://localhost:8081 (for test we use override)",
method: "post",
description:
"Demo action that shows how complex types can be used in an action",
out: {
envelope: "GResponse",
fields: [
{
name: "date",
description: "Use javascript Date class as complex type",
complex: "+Date",
},
{
name: "money",
description: "External money type",
complex: "+Money",
},
],
},
};
}
/**
* The base class definition for sampleActionRes
**/
export class SampleActionRes {
/**
* Use javascript Date class as complex type
* @type {Date}
**/
#date!: Date;
/**
* Use javascript Date class as complex type
* @returns {Date}
**/
get date() {
return this.#date;
}
/**
* Use javascript Date class as complex type
* @type {Date}
**/
set date(value: Date) {
if (value instanceof Date) {
this.#date = value;
} else {
this.#date = new Date(value);
}
}
setDate(value: Date) {
this.date = value;
return this;
}
/**
* External money type
* @type {Money}
**/
#money!: Money;
/**
* External money type
* @returns {Money}
**/
get money() {
return this.#money;
}
/**
* External money type
* @type {Money}
**/
set money(value: Money) {
if (value instanceof Money) {
this.#money = value;
} else {
this.#money = new Money(value);
}
}
setMoney(value: Money) {
this.money = value;
return this;
}
constructor(data: unknown = undefined) {
if (data === null || data === undefined) {
return;
}
if (typeof data === "string") {
this.applyFromObject(JSON.parse(data));
} else if (this.#isJsonAppliable(data)) {
this.applyFromObject(data);
} else {
throw new Error(
"Instance cannot be created on an unknown value, check the content being passed. got: " +
typeof data,
);
}
}
#isJsonAppliable(obj: unknown) {
const g = globalThis as unknown as { Buffer: any; Blob: any };
const isBuffer =
typeof g.Buffer !== "undefined" &&
typeof g.Buffer.isBuffer === "function" &&
g.Buffer.isBuffer(obj);
const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob;
return (
obj &&
typeof obj === "object" &&
!Array.isArray(obj) &&
!isBuffer &&
!(obj instanceof ArrayBuffer) &&
!isBlob
);
}
/**
* casts the fields of a javascript object into the class properties one by one
**/
applyFromObject(data = {}) {
const d = data as Partial<SampleActionRes>;
if (d.date !== undefined) {
this.date = d.date;
}
if (d.money !== undefined) {
this.money = d.money;
}
}
/**
* Special toJSON override, since the field are private,
* Json stringify won't see them unless we mention it explicitly. Each
* field goes through #toPlainJSON rather than a bare this.#field: a
* nested dto instance, or an emigo Array/One/Collection wrapper, has its
* own toJSON that JSON.stringify would only reach on a *second* pass -
* calling it here means toJSON()'s own return value is already the same
* plain shape a consumer choosing the exported type instead of this
* class gets, rather than a shallow object still holding class instances.
**/
toJSON(): SampleActionResType {
return {
date: this.#toPlainJSON(this.#date),
money: this.#toPlainJSON(this.#money),
} as SampleActionResType;
}
/**
* Resolves value into a plain, JSON-serializable value: recurses into
* arrays, and - the case JSON.stringify's own recursion would only get to
* after this method already returned - calls a nested value's own
* toJSON() (a dto instance, or an emigo Array/One/Collection wrapper)
* rather than leaving it as-is. A plain value (string, number, a map's
* own plain object, ...) is returned unchanged.
**/
#toPlainJSON(value: unknown): unknown {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => this.#toPlainJSON(item));
}
const asAny = value as any;
if (typeof asAny.toJSON === "function") {
return this.#toPlainJSON(asAny.toJSON());
}
return value;
}
toString() {
return JSON.stringify(this);
}
static get Fields() {
return {
date: "date",
money: "money",
};
}
/**
* Creates an instance of SampleActionRes, and possibleDtoObject
* needs to satisfy the type requirement fully, otherwise typescript compile would
* be complaining.
**/
static from(possibleDtoObject: SampleActionResType) {
return new SampleActionRes(possibleDtoObject);
}
/**
* Creates an instance of SampleActionRes, and partialDtoObject
* needs to satisfy the type, but partially, and rest of the content would
* be constructed according to data types and nullability.
**/
static with(partialDtoObject: PartialDeep<SampleActionResType>) {
return new SampleActionRes(partialDtoObject);
}
copyWith(
partial: PartialDeep<SampleActionResType>,
): InstanceType<typeof SampleActionRes> {
return new SampleActionRes({ ...this.toJSON(), ...partial });
}
clone(): InstanceType<typeof SampleActionRes> {
return new SampleActionRes(this.toJSON());
}
}
export abstract class SampleActionResFactory {
abstract create(data: unknown): SampleActionRes;
}
/**
* The base type definition for sampleActionRes
**/
export type SampleActionResType = {
/**
* Use javascript Date class as complex type
* @type {PlainOf<Date>}
**/
date: PlainOf<Date>;
/**
* External money type
* @type {PlainOf<Money>}
**/
money: PlainOf<Money>;
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SampleActionResType {}
  • Date is treated as a built-in complex and is not imported.
  • Money is imported from the given location and instantiated when assigned.
  • The generated setters (setDate, setMoney) return this so you can chain assignments.