Developer and practice lead trying to find the most elegant balance between user needs, business goals, and technical constraints.
Cool pattern 👍 Thanks for sharing. I have never used phantom properties before, so that’s a new thing for me, and worth to keep in the back of my head
Btw. in point 2, I think it would be easier to use `Pick` (or `Omit`) instead of writing the type of `args`, like so:
export function declareContentType(
“id” | “buildQuery” | “extractContent”
Another thing that I was wondering is whether the phantom properties are there only to be able to extract them via `ContentType` and `ArgsType` types (defined in point 3). If so, how about using:
type ContentType = T extends ContentStackContentType ? C : never;
type ArgType = T extends ContentStackContentType ? A : never;
Those let you extract the generic parameters easily without declaring phantom properties. I don’t see any cons to using this solution, and IMO it’s a bit safer, as TS matches runtime values :D
Example revised API in TS playground:
Pick or Omit would be reasonable there, but the APIs of the declaration functions often diverge over time from the underlying type. As things get more complex the ideal declaration API is often higher-level than the underlying value type. The argument type design for the declaration function changes to improve user experience for declaring instances, whereas the underlying type changes to enable new capabilities. Since they types change for different reasons, I usually opt into the redundancy to help remind myself and my team about this distinction, but YMMV.
Regarding the phantom properties, you start to run into problems when you have type arguments that aren’t reflected in the structure of the type. Two types that have different type variables can be treated as equivalent, and things that need to infer the phantom type sometimes can’t because the unused type was erased. Basically: you get a combination of types that aren’t as strict as you’d like and broken inference. It just doesn’t work.
(This was at least an issues a few versions of TypeScript back, but I haven’t re-verified this in newer 4.x versions of TypeScript, as I haven’t felt the need. It could work differently now, but the type compatibility issue, at least, is pretty fundamental to structural typing.)
I also just prefer phantom properties to ternary types/inference for this use case. To be honest, I don’t always introduce utility types for extracting out type parameters, especially when the type is more of an internal detail that isn’t really needed outside of the core ecmascript module. You can always just do `Type[‘someProp’]` to use the types without necessarily introducing helper types or forcing yourself into pattern matching. Since I usually have the phantom properties for the above correctness/functionality reason, this benefit comes along for the ride.
Comments are closed.
We’d love to talk with you about your next great software project. Fill out this form and we’ll get back to you within two business days.