I am working on creating an interface that can accept a mapped type, allowing for both runtime logic and compile-time typing to be utilized.
Here is an example of what I'm aiming for:
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 reality, magicalFunction
takes SomeType
as a generic parameter. For this discussion, let's assume it's hardcoded with SomeType
.)
I've managed to achieve the first three behaviors 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
The issue I'm facing is that when extra properties are specified, like {a:1, c:1}
, there is no compilation error. While this behavior makes sense due to all fields being optional in the inferred P
type, I am looking for a solution to enforce stricter typing. Is there a way to achieve this without losing type inference?
It seems that modifying P
during type specification for the p
parameter disrupts type inference. One potential approach could involve a key filtering type that fails for unknown keys, recursively. Changing the signature to
magicalFunction<...>(p: FilterKeys<P, SomeType>): ...
results in a call like magicalFunction({a:1})
resolving to magicalFunction<unknown>
.
background
My ultimate objective is to create a repository class that is typed to a specific entity and capable of executing projection queries against MongoDB. To ensure type safety, I aim to have auto-complete functionality for projection fields, compilation errors when specifying non-existent fields, and a return type that aligns with the projection.
For instance:
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> }