ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
310 lines (309 loc) • 11.5 kB
TypeScript
/**
* Utility types for ECSpresso ECS framework
* This file contains reusable type helpers used across the codebase
*/
import type { ScreenDefinition } from './screen-types';
/**
* Check if two types are exactly the same for overlapping keys
*/
type ExactlyCompatible<T, U> = T extends U ? U extends T ? true : false : false;
/**
* Check if two record types are compatible (no conflicting keys).
* Returns true if no overlapping keys or all overlapping keys have exactly the same type.
*/
export type TypesAreCompatible<T extends Record<string, any>, U extends Record<string, any>> = [
keyof T & keyof U
] extends [never] ? true : {
[K in keyof T & keyof U]: ExactlyCompatible<T[K], U[K]>;
}[keyof T & keyof U] extends false ? false : true;
/**
* Single config object that bundles all 5 world type dimensions.
* Replaces the 5 positional type params (ComponentTypes, EventTypes,
* ResourceTypes, AssetTypes, ScreenStates) throughout the codebase.
*/
export interface WorldConfig {
readonly components: Record<string, any>;
readonly events: Record<string, any>;
readonly resources: Record<string, any>;
readonly assets: Record<string, unknown>;
readonly screens: Record<string, ScreenDefinition<any, any>>;
}
/**
* Construct a WorldConfig from individual type dimensions.
* All parameters default to empty records.
*/
export type WorldConfigFrom<C extends Record<string, any> = {}, E extends Record<string, any> = {}, R extends Record<string, any> = {}, A extends Record<string, unknown> = {}, S extends Record<string, ScreenDefinition<any, any>> = {}> = {
readonly components: C;
readonly events: E;
readonly resources: R;
readonly assets: A;
readonly screens: S;
};
/**
* Empty WorldConfig with all slots defaulting to {}.
*/
export type EmptyConfig = WorldConfigFrom;
/**
* Construct a WorldConfig containing only component requirements.
*/
export type ComponentsConfig<C extends WorldConfig['components']> = WorldConfigFrom<C>;
/**
* Construct a WorldConfig containing only event requirements.
*/
export type EventsConfig<E extends WorldConfig['events']> = WorldConfigFrom<{}, E>;
/**
* Construct a WorldConfig containing only resource requirements.
*/
export type ResourcesConfig<R extends WorldConfig['resources']> = WorldConfigFrom<{}, {}, R>;
/**
* Construct a WorldConfig containing only asset requirements.
*/
export type AssetsConfig<A extends WorldConfig['assets']> = WorldConfigFrom<{}, {}, {}, A>;
/**
* Construct a WorldConfig containing only screen requirements.
*/
export type ScreensConfig<S extends WorldConfig['screens']> = WorldConfigFrom<{}, {}, {}, {}, S>;
/**
* Merge two WorldConfig types by intersecting each slot.
*/
export type MergeConfigs<A extends WorldConfig, B extends WorldConfig> = {
readonly components: A['components'] & B['components'];
readonly events: A['events'] & B['events'];
readonly resources: A['resources'] & B['resources'];
readonly assets: A['assets'] & B['assets'];
readonly screens: A['screens'] & B['screens'];
};
export type WithComponents<Cfg extends WorldConfig, T> = {
readonly components: Cfg['components'] & T;
readonly events: Cfg['events'];
readonly resources: Cfg['resources'];
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'];
};
export type WithEvents<Cfg extends WorldConfig, T> = {
readonly components: Cfg['components'];
readonly events: Cfg['events'] & T;
readonly resources: Cfg['resources'];
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'];
};
export type WithResources<Cfg extends WorldConfig, T> = {
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'] & T;
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'];
};
export type WithAssets<Cfg extends WorldConfig, T> = {
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'];
readonly assets: Cfg['assets'] & T;
readonly screens: Cfg['screens'];
};
export type WithScreens<Cfg extends WorldConfig, T> = {
readonly components: Cfg['components'];
readonly events: Cfg['events'];
readonly resources: Cfg['resources'];
readonly assets: Cfg['assets'];
readonly screens: Cfg['screens'] & T;
};
/**
* Union of WorldConfig slots where A and B have conflicting types for the
* same key. Empty (`never`) when the two configs are compatible.
*/
export type ConflictingSlot<A extends WorldConfig, B extends WorldConfig> = (TypesAreCompatible<A['components'], B['components']> extends true ? never : 'components') | (TypesAreCompatible<A['events'], B['events']> extends true ? never : 'events') | (TypesAreCompatible<A['resources'], B['resources']> extends true ? never : 'resources') | (TypesAreCompatible<A['assets'], B['assets']> extends true ? never : 'assets') | (TypesAreCompatible<A['screens'], B['screens']> extends true ? never : 'screens');
/**
* Union of WorldConfig slots where Required has keys not present in
* Accumulated. Empty (`never`) when all requirements are satisfied.
*/
export type MissingRequirementSlot<Accumulated extends WorldConfig, Required extends WorldConfig> = (keyof Required['components'] extends keyof Accumulated['components'] ? never : 'components') | (keyof Required['events'] extends keyof Accumulated['events'] ? never : 'events') | (keyof Required['resources'] extends keyof Accumulated['resources'] ? never : 'resources') | (keyof Required['assets'] extends keyof Accumulated['assets'] ? never : 'assets') | (keyof Required['screens'] extends keyof Accumulated['screens'] ? never : 'screens');
/**
* Check if two WorldConfig types are compatible (no conflicting keys
* across any slot).
*/
export type ConfigsAreCompatible<A extends WorldConfig, B extends WorldConfig> = [
ConflictingSlot<A, B>
] extends [never] ? true : false;
/**
* Check if a Requires config is satisfied by an Accumulated config.
* Checks all five WorldConfig slots (components, events, resources, assets, screens).
* When Required is EmptyConfig, all slots have `keyof {} = never`,
* and `never extends X = true`, so empty requirements are always satisfied.
*/
export type RequirementsSatisfied<Accumulated extends WorldConfig, Required extends WorldConfig> = [
MissingRequirementSlot<Accumulated, Required>
] extends [never] ? true : false;
/**
* Utility type for merging two types
*/
export type Merge<T1, T2> = T1 & T2;
/**
* Utility type for merging an array of types
*/
export type MergeAll<T extends any[]> = T extends [infer First, ...infer Rest] ? Rest extends [] ? First : Merge<First, MergeAll<Rest>> : {};
/**
* Wildcard ECSpresso type that any concrete instance is assignable to.
* Use as a generic constraint for functions that accept any ECSpresso world.
* Matches the phantom _cfg property declared on the ECSpresso class.
*/
export type AnyECSpresso = {
readonly _cfg: WorldConfig;
};
/**
* Wildcard Plugin type that any concrete plugin is assignable to.
* Matches the phantom _cfg and _requires properties declared on the Plugin interface.
*/
export type AnyPlugin = {
readonly _cfg?: WorldConfig;
readonly _requires?: WorldConfig;
};
/**
* Extract the full WorldConfig from a Plugin or ECSpresso instance
*/
export type ConfigOf<B> = B extends {
readonly _cfg: infer Cfg extends WorldConfig;
} ? Cfg : never;
/**
* Extract the ComponentTypes from a Plugin or ECSpresso instance
*/
export type ComponentsOf<B> = B extends {
readonly _cfg: {
components: infer C extends Record<string, any>;
};
} ? C : B extends {
readonly _cfg?: {
components: infer C extends Record<string, any>;
};
} ? C : never;
/**
* Extract the EventTypes from a Plugin or ECSpresso instance
*/
export type EventsOf<B> = B extends {
readonly _cfg: {
events: infer E extends Record<string, any>;
};
} ? E : B extends {
readonly _cfg?: {
events: infer E extends Record<string, any>;
};
} ? E : never;
/**
* Extract the ResourceTypes from a Plugin or ECSpresso instance
*/
export type ResourcesOf<B> = B extends {
readonly _cfg: {
resources: infer R extends Record<string, any>;
};
} ? R : B extends {
readonly _cfg?: {
resources: infer R extends Record<string, any>;
};
} ? R : never;
/**
* Extract AssetTypes from a Plugin or ECSpresso instance
*/
export type AssetTypesOf<B> = B extends {
readonly _cfg: {
assets: infer A extends Record<string, unknown>;
};
} ? A : B extends {
readonly _cfg?: {
assets: infer A extends Record<string, unknown>;
};
} ? A : never;
/**
* Extract ScreenStates from a Plugin or ECSpresso instance
*/
export type ScreenStatesOf<B> = B extends {
readonly _cfg: {
screens: infer S extends Record<string, ScreenDefinition<any, any>>;
};
} ? S : B extends {
readonly _cfg?: {
screens: infer S extends Record<string, ScreenDefinition<any, any>>;
};
} ? S : never;
/**
* Extract the system Labels from a Plugin instance
*/
export type LabelsOf<B> = B extends {
readonly _labels?: infer L;
} ? L extends string ? L : never : never;
/**
* Extract the system Groups from a Plugin instance
*/
export type GroupsOf<B> = B extends {
readonly _groups?: infer G;
} ? G extends string ? G : never : never;
/**
* Extract the AssetGroupNames from a Plugin instance
*/
export type AssetGroupNamesOf<B> = B extends {
readonly _assetGroupNames?: infer AG;
} ? AG extends string ? AG : never : never;
/**
* Extract the ReactiveQueryNames from a Plugin instance
*/
export type ReactiveQueryNamesOf<B> = B extends {
readonly _reactiveQueryNames?: infer RQ;
} ? RQ extends string ? RQ : never : never;
/**
* Extract ComponentTypes from an ECSpresso world instance type.
*/
export type ComponentsOfWorld<W> = W extends {
readonly _cfg: {
components: infer C extends Record<string, any>;
};
} ? C : never;
/**
* Extract EventTypes from an ECSpresso world instance type.
*/
export type EventsOfWorld<W> = W extends {
readonly _cfg: {
events: infer E extends Record<string, any>;
};
} ? E : never;
/**
* Extract AssetTypes from an ECSpresso world instance type.
*/
export type AssetsOfWorld<W> = W extends {
readonly _cfg: {
assets: infer A extends Record<string, unknown>;
};
} ? A : never;
/**
* Extract ScreenStates from an ECSpresso world instance type
*/
export type ScreenStatesOfWorld<W> = W extends {
readonly _cfg: {
screens: infer S extends Record<string, ScreenDefinition<any, any>>;
};
} ? S : never;
/**
* Extract event names from an EventTypes record whose payload extends the given shape.
* Eliminates the need for each plugin to define its own mapped filter type.
*
* @example
* ```typescript
* interface MyEventData { entityId: number }
* type MyEventName<ET> = EventNameMatching<ET, MyEventData>;
* ```
*/
export type EventNameMatching<ET extends Record<string, any>, Payload> = {
[K in keyof ET & string]: ET[K] extends Payload ? K : never;
}[keyof ET & string];
/**
* Extract the channel type from a world's AudioSource component.
* Falls back to `string` if the world has no audioSource component.
*/
export type ChannelOfWorld<W> = W extends {
readonly _cfg: {
components: {
audioSource: {
channel: infer Ch extends string;
};
};
};
} ? Ch : string;
export {};