Skip to content

Instantly share code, notes, and snippets.

@igabesz
Created May 22, 2021 11:55
Show Gist options
  • Save igabesz/44dca1805e6cdad3224e5588469ad5c2 to your computer and use it in GitHub Desktop.
Save igabesz/44dca1805e6cdad3224e5588469ad5c2 to your computer and use it in GitHub Desktop.
[Metaprogramming] TypeScript Decorators with Type Inference

Keywords:

  • TypeScript
  • Decorators
  • Type Inference
  • Metaprogramming

What do I do here?

I want to move some boilerplate code to decorators. The background FW loads and saves at certain times. Currently this is quite custom -- but the actual requirements are not that custom. I want to make a more standardized way for this.

Why is it good?

I'm not sure yet if it's the best -- or even significantly better than the current approach. However, I think that having a standardized way might help avoiding some of the issues we've encountered recently. As with FW development generally: it might help developers to be able to create better code.

So it enforces a few constraints, e.g. having to use explicit types for saved objects, controlled way of handling data, etc. But good constraints, I think.

The good stuff

We've got pretty good type constraints. The config of the Store6 decorator is well-typed, it should help developers when something doesn't add up. It mostly follows the actual standards we use. Missing features:

  • Load raw data without transforming first
  • Support lifecycle hooks

The bad stuff

I don't like that the decorator template types are to be declared explicitly.
But I couldn't make it work implicitly yet.

Also, the error messages are not always helpful. If the type of the decorator and the actual member are not in sync, then I enforce a never to the target type. The user will see that something is wrong, but it won't be that helpful.

Will I use it?

Maybe.

// Historical stuff
interface ReadWritable<T> {
read(): T;
write(): T;
}
// Get the payload of an array -- or never.
type ArrayPayload<T> = T extends (infer U)[] ? U : never;
// This way we can get the type of a private variable.
// https://stackoverflow.com/questions/67644650/
type UniversallyIndexable<T> = T & { [key: string]: never };
type UniversalMember<T, TMember extends string> = UniversallyIndexable<T>[TMember];
// Good thing: Awesome build-time constraints.
// Bad thing: Always have to specify the 2-3 params.
// Also bad: The error messages are not always helpful.
function Store6<T, TMemberType, TStoredType = TMemberType>(config?: {
// Load back to the RW directly
loadDirectly?: boolean,
// Expicit loader func
loader?: (self: T) => (value: TMemberType) => any,
// Items loader. Works only if `TMemberType` is an array.
itemLoader?: <TItem extends ArrayPayload<TMemberType>>(self: T) => (value: TItem) => any,
// Optional: transform before save
saveTransform?: (value: TMemberType) => TStoredType,
// Optional: transform before load
loadTransform?: (value: TStoredType) => TMemberType,
}) {
// Here we guarantee that the `T` has a `TMember` property with `ReadWritable<TMemberType>` type.
// The fun is that it works for private properties too!
return <
TMember extends UniversalMember<T, TMember> extends ReadWritable<TMemberType> ? string : never,
>(t: T, memberName: TMember) => {};
}
class Dummy6 {
// Simple loader stuff
@Store6<Dummy6, Payload>({
loader: self => self.loadPayload,
})
private readonly item!: ReadWritable<Payload>;
// Item loader: it's an array, so it works.
@Store6<Dummy6, number[]>({
itemLoader: self => self.loadArrayItem,
})
private readonly arr!: ReadWritable<number[]>;
// The saved type is different. Here we use the 2 transforms.
@Store6<Dummy6, Date, number>({
loadDirectly: true,
loadTransform: value => new Date(value),
saveTransform: date => date.getDate(),
})
private readonly date!: ReadWritable<Date>;
loadPayload(p: Payload) {}
loadArrayItem(value: number) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment