I'm trying to provide an interface that accepts a map of a given type, and uses it for both runtime logic and compile-time typing.
something like:
type SomeType = {
a: string
b: { a: string, b: string }
}
magicalFunction({ a: 1 }) // 1. return type is {a: string}
magicalFunction({ b: 1 }) // 2. return type is { b: { a: string, b: string } }
magicalFunction({ b: { a: 1 } }) // 3. return type is { b: { a: string } }
magicalFunction({ a: 1, c: 1 }) // 4. compile-time error since there's no 'c' on SomeType
(in the real world, magicalFunction
takes SomeType
as a generic parameter. for this question we can assume it's just hard-coded with SomeType
.)
I'm getting the first 3 behaviours when using mapped types:
export type ProjectionMap<T> = {
[k in keyof T]?: T[k] extends object
? 1 | ProjectionMap<T[k]>
: 1
export type Projection<T, P extends ProjectionMap<T>> = {
[k in keyof T & keyof P]: P[k] extends object
? Projection<T[k], P[k]>
: T[k]
}
type SomeType = {
a: string
b: { a: string, b: string }
}
function magicalFunction<P extends ProjectionMap<SomeType>>(p: P): Projection<SomeType, P> {
/* using `p` to do some logic and construct something that matches `P` */
throw new Error("WIP")
}
const res = magicalFunction({ a:1 })
// etc
Problem is I'm not getting a compilation error when specifying extra properties - e.g {a:1, c:1}
. Now, it actually makes perfect sense - in that case the compiler infers P
as typeof { a:1, c:1 }
, which is a valid subclass of the result of ProjectionMap<SomeType>
since all fields are optional. But is there some magical incantation that will make this work as I described? A different way to specify the type, or some mapped-types magic?
As far as I can tell, manipulating P
when specifying the type for the p
parameter breaks type inference. If we assume there's a key filtering type (fails for unknown keys, recursively), then changing the signature to magicalFunction<...>(p: FilterKeys<P, SomeType>): ...
makes a call like magicalFunction({a:1})
resolve to magicalFunction<unknown>
.
background
my end-goal here is to create a repository class, which is typed to a specific entity, and can perform projection queries against mongo. For type safety I want to get auto-complete for the projection fields, compilation errors when specifying non-existing fields, and a return type that matches the projection.
for example:
class TestClass { num: number, str: string }
const repo = new Repo<TestClass>()
await repo.find(/*query*/, { projection: { num: 1 } })
// compile-time type: { num: number}
// runtime instance: { num: <some_concrete_value> }