I’m working on a component that fetches records from a data source. The component is written in TypeScript which I’m not super comfortable with.
Each record is characterized by a key and the record’s value type. The fetch method takes an instance of RecordDescriptor<Value> whose generic parameter Value determines the method’s return type.
In Swift, I would accomplish the goal with the following code, taking advantage of metatypes:
struct RecordDescriptor<Value> {
let key: String
let valueType: Value.Type // `Value.Type` is a metatype, the type of a type
}
func fetchValue<Value>(for descriptor: RecordDescriptor<Value>) -> Value {
// …
}
let intRecordDescriptor = RecordDescriptor(key: "int_record", valueType: Int.self)
// `Int.self` refers to the `Int` type itself, not an instance of `Int`
let intValue = fetchValue(for: intRecordDescriptor)
Basically I get to specialize the generic parameter Value without using concrete instances of a type—only the type name itself.
How do I achieve the same result in TypeScript?
You don't care about Metatypes for their type-hinting capabilities, you only care about them for their ability to
access initializers or other static members of the class or protocol
That's quite simply spelled as:
interface Newable {
new(): any
}
type Constructable<T> =
T extends Newable
? T
: T extends (...args: any[]) => unknown
? T : never
type RecordDescriptor<T, Key extends string> = { key: Key }
& (
T extends Constructable<T>
? { transformation: T }
// Replace this arm with `never`
// to not allow scalars / non-classes like `boolean`
: { transformation: (arg?: unknown) => T }
)
Usage is:
let descriptorN: RecordDescriptor<Number, "numbers"> = {
key: "numbers",
transformation: Number
}
let resultN: Number = descriptorN.transformation("123")
let descriptorF: RecordDescriptor<(a: string, b: boolean) => string | boolean, "crazy"> =
{ key: "crazy", transformation: (a, b) => b ? "hi" : a.length > 3 }
let resultF: string | boolean = descriptorF.transformation("hmm", true)
let rp: RecordDescriptor<boolean, "options"> =
{ key: "options", transformation: () => true }
let resultP: boolean = rp.transformation()