Skip to main content

๐Ÿ‘ท Best Practices

All of the convenience of Digital Alchemy comes at the expense of the utility types needed to make that work. There's a number of ways you can alter that process outside of your primary logic to alter the way your project builds.

The internals of Typescript can operate in some weird ways occasionally.

Service Definitionsโ€‹

Needing to do transformations and infer things is a frequent source of friction between this framework and the Typescript transpiler.

These can lead to laggy editing experiences at the best of times, and incorrect builds at the worst. Both situations can corrected by providing explicit interfaces for your service in an external file.

definitions.ts

export type SpecialLogicOperations = {
/**
* extra long tsdoc description on how to use this method
*/
exec: () => Promise<void>
}

special-logic.service.ts

export function SpecialLogic(): SpecialLogicOperations {
return {
async exec() {
// ๐Ÿš€ do stuff
}
}
}

When TServiceParams gets built, your provided type will be directly wired in instead of needing to be inferred each time

function Example({ my_module }: TServiceParams) {
// โœ… refers to SpecialLogicOperations
my_module.special_logic

// โœ… has attached tsdoc
my_module.special_logic.exec
}

TSDocโ€‹

A full guide on creating tsdoc is best elsewhere, but some key points to keep in mind:

  • โœ… multiline comment
  • โœ… markdown
  • โŒ html

Modulesโ€‹

Attaching TSDoc to higher level TServiceParams provided values has to be done at the module level.

Configurationโ€‹

CreateLibrary({
configuration: {
/**
* ## โš ๏ธ KEEP THIS OFF AT RUNTIME!!
*
* > For testing only
*/
DESTROY_ALL_HUMANS: {
type: "boolean",
default: false
}
},
name: "skynet"
});

Servicesโ€‹

note: If a particular service is intended to be internal only, mark with @internal

CreateApplication({
services: {
/**
* Operations for additional runtime configurations
*/
config: ConfigurationService,
},
name: "special_app"
})

Configuration Definitionsโ€‹

Fully type checking configuration definitions requires a bit of "unique" typescript. Taking the example of LOG_LEVEL:

Using satisfiesโ€‹

When using satisfies, the data provided as part of the definition itself is type checked. In the below example, the invalid value would throw a build error

CreateLibrary({
configuration: {
LOG_LEVEL: {
default: "trace",
enum: ["INVALID VALUE", "silent", "trace",...],
type: "string",
} satisfies StringConfig<TConfigLogLevel>
}
})

Using asโ€‹

The problem with using only satisfies appears when the configuration is used inside a service -

function Example({ config }: TServiceParams) {
config.boilerplate.LOG_LEVEL // presents as a generic string, instead of the expected TConfigLogLevel
}

In order to force correct casting in all locations, as well as type check your own definition, you need to use as in addition to satisfies.

CreateLibrary({
configuration: {
LOG_LEVEL: {
default: "trace",
enum: ["silent", "trace",...],
type: "string",

// You may not like it but this horror is peak performance
// I certainly don't
} satisfies StringConfig<TConfigLogLevel> as StringConfig<TConfigLogLevel>
}
})

Using only as is acceptable also if you don't care about type checking your definition.