The original example had a value like {_id: 10}
assigned to a generic type like Partial<T>
where T extends IDocument
. In such cases you can show that the compiler is correct to complain, since there are some types T
that extend IDocument
where 10
is not a valid property. This is essentially the same issue as in this question.
The new example where you assign {_id: doc._id}
to the generic type Partial<T>
where T extends IDocument
still yields an error, even though it should definitely be safe. The compiler is unable to verify that Pick<T, "_id">
is assignable to Partial<T>
. This is (related to) an open issue; the compiler currently can't do the sort of type analysis necessary to ensure this. In situations where you are sure something is safe (and you've double and triple checked) but the compiler isn't, you might want to use a type assertion:
testGeneric<T>({ _id: doc._id } as Partial<T>); // okay now
So inside the implementation of testConstraint()
you may need to use a type assertion or the equivalent (such as a single call-signature overload with a looser implementation signature).
Finally, you said you wanted to actually prevent someone from calling testConstraint<T>()
with a T
whose properties are narrower than those on IDocument
. This is more restrictive than T extends IDocument
, and is more cumbersome to represent in TypeScript where property narrowing is a natural part of subtyping. You can do it by making the generic constraint include a conditional mapped type, like this:
function testConstraint<
T extends IDocument &
{ [K in keyof IDocument]: IDocument[K] extends T[K] ? T[K] : never }>(doc: T): void;
function testConstraint(doc: IDocument) {
testGeneric({ _id: doc._id });
}
Here we have T
constrained to both IDocument
and a type where each property of IDocument
is compared to the corresponding property of T
. If the one on T
is no narrower than the one on IDocument
, great. Otherwise, the property in the constraint is narrowed all the way to never
, to which T
likely will not match.
The call signature is complicated enough that the types inside the implementation would really confuse the compiler. That's why I made it an overload, and loosened the implementation signature to be entirely non-generic. You might be able to do something generic for the implementation, but the point is that you should probably treat the call side and the implementation separately in terms of types.
Let's see that function in action:
interface TheVeryFirstDocument extends IDocument {
_id: 1
}
declare const tv1d: TheVeryFirstDocument;
testConstraint(tv1d); // error!
// ---------> ~~~~~
// Types of property '_id' are incompatible.
// Type '1' is not assignable to type 'never'.(
That gives an error, as we desire, while the following works without error:
declare const doc: IDocument;
testConstraint(doc); // okay
interface ExtendedDocument extends IDocument {
title: string;
numPages: number;
}
declare const xDoc: ExtendedDocument;
testConstraint(xDoc); // okay
Okay, hope that helps; good luck! Link to code