问题
In the following example, I can't think of any situation where assigning Pick<Object, Key> to Partial<Object> would not be sound, therefore I would expect this to be allowed.
Can anyone clarify why it is not allowed?
const fn = <T, K extends keyof T>(partial: Partial<T>, picked: Pick<T, K>) => {
/*
Type 'Pick<T, K>' is not assignable to type 'Partial<T>'.
Type 'keyof T' is not assignable to type 'K'.
'keyof T' is assignable to the constraint of type 'K', but 'K' could be instantiated with a different subtype of constraint 'string | number | symbol'.
*/
partial = picked;
};
TypeScript playground example
回答1:
@TitianCernicovaDragomir is essentially correct that the compiler generally is unable to do sophisticated type analysis on unresolved generic types. It does much better with concrete types. See Microsoft/TypeScript#28884 for a discussion about this with Pick and Omit with complementary sets of keys.
In these situations the only way to proceed is for you to personally verify that the assignment is sound and then use a type assertion as in partial = picked as Partial<T>...
... but I wouldn't do that in this case. The error really is a good one here, although it's hard to see why since you've essentially just overwritten the partial variable and done nothing with it within the function scope. So despite being unsound the code is harmless because it hasn't been allowed to wreak havoc elsewhere. Let's unchain it by making fn() return the modified partial variable:
const fn = <T, K extends keyof T>(partial: Partial<T>, picked: Pick<T, K>) => {
partial = picked; // error, for good reason
return partial; // 😈
};
So, the basic problem is that Pick<T, K> is a wider type than T. It contains the properties from T with keys in K, but it is not known not to contain properties with keys not in K. I mean, a value of type Pick<{a: string, b: number}, "a"> may well have a b property. And if it does have one, it does not have to be of type number. So it's a mistake to assign a value of type Pick<T, K> to a variable of type Partial<T>.
Let's flesh this out with a silly example. Imagine you have a Tree interface and an object of type Tree, like this:
interface Tree {
type: string;
age: number;
bark: string;
}
const tree: Tree = {
type: "Aspen",
age: 100,
bark: "smooth"
};
And you also have a Dog interface and an object of type Dog, like this:
interface Dog {
name: string;
age: number;
bark(): void;
}
const dog: Dog = {
name: "Spot",
age: 5,
bark() {
console.log("WOOF WOOF!");
}
};
So, dog and tree both have a numeric age property, and they both have a bark property of differing types. One is a string and the other is a method. Do note that dog is a perfectly valid value of type Pick<Tree, "age">, but an invalid value of type Partial<Tree>. And therefore when you call fn():
const partialTree = fn<Tree, "age">(tree, dog); // no error
my modified fn() returns dog as Partial<Tree>, and fun begins:
if (partialTree.bark) {
partialTree.bark.toUpperCase(); // okay at compile time
// at runtime "TypeError: partialTree.bark.toUpperCase is not a function"
}
That unsoundness leaked through precisely because Pick<T, K> is not known to exclude or otherwise constrain the "unpicked" properties. You can create your own StrictPicked<T, K> in which the properties from T not in K are explicitly excluded:
type StrictPicked<T, K extends keyof T> = Pick<T, K> &
Partial<Record<Exclude<keyof T, K>, never>>;
And now your code is more sound (ignoring weird things like K being a branded type like in the above comment)... but the compiler still can't verify it:
const fn2 = <T, K extends keyof T>(
partial: Partial<T>,
picked: StrictPicked<T, K>
) => {
partial = picked; // also error
partial = picked as Partial<T>; // have to do this
return partial;
};
That's still the basic issue here; the compiler can't easily deal with things like this. Maybe it will someday? But at least it's not as easily misused on the caller side:
fn2<Tree, "age">(tree, dog); // error, dog is not a StrictPicked<Tree, "age">
Anyway, hope that helps. Good luck!
Link to code
来源:https://stackoverflow.com/questions/56788853/typescript-assigning-pickt-k-to-partialt