Skip to content

Instantly share code, notes, and snippets.

@flensrocker
Last active March 6, 2024 13:23
Show Gist options
  • Save flensrocker/a77fff4fcb06c3de0fd0504fd4fac195 to your computer and use it in GitHub Desktop.
Save flensrocker/a77fff4fcb06c3de0fd0504fd4fac195 to your computer and use it in GitHub Desktop.
Helper types and functions for mapping state etc. to "named state" for @ngrx/signalStore extensions like "withEntities". Example "withError" extension included.
/**
* Prefix every property of the type with the given name.
* Ignores properties with non-string names.
*/
export type NamedObject<Name extends string, T> = {
[K in keyof T as `${Name}${K extends string ? Capitalize<K> : never}`]: T[K];
};
type KeySuffix = 'Key';
/**
* Helper type to extract all property names as keys into a new type.
* Every property gets a suffix of "Key" and its type is the original name of the property.
* @example
* type State = {
* prop: string;
* 0: number;
* };
* type StateKeys = ObjectKeys<State>;
* // = {
* // propKey: 'prop';
* //}
* @example
* // create a compile-time checked mapping
* const stateKeys: StateKeys = {
* propKey: 'prop',
* };
*/
export type ObjectKeys<T> = {
readonly [K in keyof T as `${K extends string ? K : never}${KeySuffix}`]: K;
};
/**
* Helper type to extract all property names as keys into a new type.
* Every property gets a suffix of "Key" and its type is the capitalized name of the property.
* This is an intermediate step towards named object keys.
* @see NamedObjectKeys
* @example
* type State = {
* prop: string;
* 0: number;
* };
* type StateKeysCapitalized = ObjectKeysCapitalized<State>;
* // = {
* // propKey: 'Prop';
* //}
* @example
* // create a compile-time checked mapping
* const stateKeysCapitalized: StateKeysCapitalized = {
* propKey: 'Prop',
* };
*/
export type ObjectKeysCapitalized<T> = {
readonly [K in keyof T as `${K extends string
? K
: never}${KeySuffix}`]: K extends string ? Capitalize<K> : never;
};
/**
* Helper type to extract all property names as keys into a new type.
* Every property gets a suffix of "Key" and its type is the capitalized name of the property prefixed with the given name.
* The keys matches those of {@type ObjectKeys} and {@type ObjectKeysCapitalized}.
* It's used as a return type of {@link createNamedObjectKeys} and {@link getObjectKeys}.
*/
export type NamedObjectKeys<Name extends string, T> = {
readonly [K in keyof ObjectKeysCapitalized<T>]: `${Name}${ObjectKeysCapitalized<T>[K]}`;
};
/**
* Creates an object with keys, which correspond to the keys of the given {@type T}.
* They get a suffix of "Key".
* Their type is the capitalized name of the original property prefixed with the given {@param name}.
* @param name The prefix for all property types.
* @param objectKeysCapitalized An object derived from the original type {@type T} with capitalized keytypes.
* @returns A map which maps a property name to its prefixed name.
* @example
* type State = {
* prop: string;
* };
* type StateKeysCapitalized = ObjectKeysCapitalized<State>;
*
* const stateKeysCapitalized: StateKeysCapitalized = {
* propKey: 'Prop',
* };
*
* const { propKey } = createNamedObjectKeys("dummy", stateKeysCapitalized);
* // propKey === "dummyProp"
*/
export const createNamedObjectKeys = <Name extends string, T>(
name: Name,
objectKeysCapitalized: ObjectKeysCapitalized<T>
): NamedObjectKeys<Name, T> => {
return Object.keys(objectKeysCapitalized).reduce(
(obj, key) => ({
...obj,
[key]: `${name}${
(objectKeysCapitalized as Record<string, unknown>)[key]
}`,
}),
{} as Record<string, string>
) as NamedObjectKeys<Name, T>;
};
/**
* Returns an object with named keys like {@link createNamedObjectKeys} if a name is given.
* Otherwise {@param objectKeys} is returned.
* Useful for typesafe conditional mapping of a type to an object with dynamic keys.
* @param name The prefix for all property types.
* @param objectKeys An object derived from the original type {@type T} with a property/name mapping.
* @param objectKeysCapitalized An object derived from the original type {@type T} with a property/capitalized-name mapping.
* @returns A map which maps a property name to its optional prefixed name.
* @example
* type State = {
* prop: string;
* };
* type NamedState<Name extends string> = NamedObject<Name, State>;
*
* type StateKeys = ObjectKeys<State>;
* type StateKeysCapitalized = ObjectKeysCapitalized<State>;
*
* const stateKeys: StateKeys = {
* propKey: 'prop',
* };
* const stateKeysCapitalized: StateKeysCapitalized = {
* propKey: 'Prop',
* };
*
* function setProp(state: State, prop: string): Partial<State<;
* function setProp<Name extends string>(state: NamedState<Name>, prop: string, name: Name): Partial<NamedState<Name>>;
* function setProp<Name extends string>(state: State | NamedState<Name>, prop: string, name?: Name): Partial<State | NamedState<Name>> {
* const { propKey } = getObjectKeys(name, stateKeys, stateKeysCapitalized);
* return {
* [propKey]: prop,
* };
* }
*/
export const getObjectKeys = <Name extends string, T>(
name: Name | undefined,
objectKeys: ObjectKeys<T>,
objectKeysCapitalized: ObjectKeysCapitalized<T>
): ObjectKeys<T> | NamedObjectKeys<Name, T> => {
if (typeof name === 'string') {
return createNamedObjectKeys(name, objectKeysCapitalized);
}
return objectKeys;
};
import { Signal, computed } from '@angular/core';
import {
SignalStoreFeature,
signalStoreFeature,
withComputed,
withState,
} from '@ngrx/signals';
import {
NamedObject,
ObjectKeys,
ObjectKeysCapitalized,
getObjectKeys,
} from './object-keys';
// An example extension to @ngrx/signalStore which adds an "error" field and "hasError" signal to the store.
//
// const ExampleStore = signalStore(
// withState(...),
// withError(), // adds "error" and "hasError"
// ...
// );
//
// const NamedExampleStore = signalStore(
// withState(...),
// withError("foo"), // adds "fooError" and "fooHasError"
// withError("bar"), // adds "barError" and "barHasError"
// ...
// );
export type ErrorState = {
readonly error: string | null;
};
export type ErrorSignals = {
readonly hasError: Signal<boolean>;
};
export type NamedErrorState<Collection extends string> = NamedObject<
Collection,
ErrorState
>;
export type NamedErrorSignals<Collection extends string> = NamedObject<
Collection,
ErrorSignals
>;
type ErrorStateKeys = ObjectKeys<ErrorState>;
type ErrorStateKeysCapitalized = ObjectKeysCapitalized<ErrorState>;
type ErrorSignalsKeys = ObjectKeys<ErrorSignals>;
type ErrorSignalsKeysCapitalized = ObjectKeysCapitalized<ErrorSignals>;
const errorStateKeys: ErrorStateKeys = {
errorKey: 'error',
};
const errorStateKeysCapitalized: ErrorStateKeysCapitalized = {
errorKey: 'Error',
};
const errorSignalsKeys: ErrorSignalsKeys = {
hasErrorKey: 'hasError',
};
const errorSignalsKeysCapitalized: ErrorSignalsKeysCapitalized = {
hasErrorKey: 'HasError',
};
export function withError(): SignalStoreFeature<
{
state: NonNullable<unknown>;
signals: NonNullable<unknown>;
methods: NonNullable<unknown>;
},
{
state: ErrorState;
signals: ErrorSignals;
methods: NonNullable<unknown>;
}
>;
export function withError<Collection extends string>(
collection: Collection
): SignalStoreFeature<
{
state: NonNullable<unknown>;
signals: NonNullable<unknown>;
methods: NonNullable<unknown>;
},
{
state: NamedErrorState<Collection>;
signals: NamedErrorSignals<Collection>;
methods: NonNullable<unknown>;
}
>;
export function withError<Collection extends string>(
collection?: Collection
): SignalStoreFeature {
const { errorKey } = getObjectKeys(
collection,
errorStateKeys,
errorStateKeysCapitalized
);
const { hasErrorKey } = getObjectKeys(
collection,
errorSignalsKeys,
errorSignalsKeysCapitalized
);
return signalStoreFeature(
withState({
[errorKey]: null,
}),
withComputed((store: Record<string, Signal<unknown>>) => {
const error = store[errorKey] as Signal<ErrorState['error']>;
return {
[hasErrorKey]: computed(() => error() != null),
};
})
);
}
export function setError(error: string): Partial<ErrorState>;
export function setError<Collection extends string>(
collection: Collection,
error: string
): Partial<NamedErrorState<Collection>>;
export function setError<Collection extends string>(
collectionOrError: Collection | string,
error?: string
): Partial<NamedErrorState<Collection> | ErrorState> {
const { errorKey } = getObjectKeys(
collectionOrError,
errorStateKeys,
errorStateKeysCapitalized
);
const realError =
typeof error === 'string'
? error.length === 0
? null
: error
: collectionOrError.length === 0
? null
: collectionOrError;
return {
[errorKey]: realError,
};
}
export function clearError(): Partial<ErrorState>;
export function clearError<Collection extends string>(
collection: Collection
): Partial<NamedErrorState<Collection>>;
export function clearError<Collection extends string>(
collection?: Collection
): Partial<NamedErrorState<Collection> | ErrorState> {
const { errorKey } = getObjectKeys(
collection,
errorStateKeys,
errorStateKeysCapitalized
);
return {
[errorKey]: null,
};
}
@flensrocker
Copy link
Author

Try to rename the error property on ErrorState. The compiler will lead you to all the places where you have to correct the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment