Reputation: 1010
Assuming the given type:
export declare type MongoManagerFilter<T> = {
[P in keyof T]?: (P extends keyof T ? T[P] : any);
};
When trying to specify the filter:
update.$setOnInsert.createdAt = new Date();
I get this error:
Type 'Date' is not assignable to type '"createdAt" extends keyof T ? T[keyof T & "createdAt"] : any'.ts(2322)
But if I change the filter to this it works:
export declare type MongoManagerFilter<T> = {
[P in keyof T]?: any;
};
Why does the conditional statement not actually figure out the type? It's just keeping the code as the type.
I know the filter doesn't make sense how it is, I have another type instead of keyof T
to catch dot notation, but I am having the same issue with conditional statements not figuring out the actual type.
interface CollectionDocument {
_id?: string;
createdAt: Date;
}
type PathImpl<T, Key extends keyof T> =
Key extends string
? T[Key] extends Record<string, any>
? `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string>}`
| `${Key}.${Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string}`
: never
: never;
type Path<T> = keyof T | PathImpl<T, keyof T>;
type PathValue<T, P extends Path<T>> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T
? T[P]
: any;
export declare type MongoManagerFilter<T> = {
[P in Path<T>]?: PathValue<T, P>;
};
export class MongoManagerCollection<T extends CollectionDocument> {
constructor() {}
updateOne(filter: MongoManagerFilter<T>) {
// THIS DOES NOT WORK
filter._id = '';
filter.createdAt = new Date();
}
}
// THIS WORKS
let testModel = new MongoManagerCollection<CollectionDocument>();
testModel.updateOne({_id: 'test', createdAt: new Date()});
EDIT Adding another layer to this playground for more issues along same lines:
Seems I am getting this error as well due to the recursion in the types:
Type instantiation is excessively deep and possibly infinite.ts(2589)
Upvotes: 1
Views: 283
Reputation: 33041
Generic type T
in:
export class MongoManagerCollection<T extends CollectionDocument> {
constructor() { }
updateOne(filter: MongoManagerFilter<T>) {
// THIS DOES NOT WORK
filter._id = '';
filter.createdAt = new Date();
}
}
is a black box. filter._id
is resolved as a non evaluated conditional type. Treat it as a non called function. Function which is has never been called does not return any value. Same is with filter._id
.
filter._id
coresponds to this:
type PathValue<T, P extends Path<T>> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T ////////////////////////////////////////////////
? T[P] // Property is resolved as a conditional type //
: any; ///////////////////////////////////////////////
As you might have noticed, filter._id
is P extends keyof T ? T[P] : any
instead of just T[P]
.
There is a workaround. You can use T[P & keyof T]
instead of conditional type
.
See working example:
interface CollectionDocument {
_id?: string;
createdAt: Date;
}
type PathImpl<T, Key extends keyof T> =
Key extends string
? T[Key] extends Record<string, any>
? `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string>}`
| `${Key}.${Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string}`
: never
: never;
type Path<T> = keyof T | PathImpl<T, keyof T>;
type PathValue<T, P extends Path<T>> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: T[P & keyof T]
export declare type MongoManagerFilter<T> = {
[P in Path<T>]?: PathValue<T, P>;
};
export class MongoManagerCollection<T extends CollectionDocument> {
constructor() { }
updateOne(filter: MongoManagerFilter<T>) {
filter._id = ''; // ok
filter.createdAt = new Date(); // ok
filter.createdAt = 'sdf'; // expected error
}
}
// THIS WORKS
let testModel = new MongoManagerCollection<CollectionDocument>();
testModel.updateOne({ _id: 'test', createdAt: new Date() });
If you are interested in path
implementation, you can take a look at this answer, this utility and/or my article.
I don't know MongoDB api and the requirements of your types so I can't say whether they need to be modified or not.
Also, you can get rid of generic :
export class MongoManagerCollection {
constructor() { }
updateOne(filter: MongoManagerFilter<CollectionDocument>) {
filter._id = ''; // ok
filter.createdAt = new Date(); // ok
}
}
// THIS WORKS
let testModel = new MongoManagerCollection();
testModel.updateOne({ _id: 'test', createdAt: new Date() });
but I don't think this is what you want.
Upvotes: 1