You want exact types, which don't really exist in TypeScript. Object types in TypeScript are considered "open" or "extendible"; a type like {a?: string, b?: string, c?: string}
only specifies what kind of properties are at the a
, b
, and c
keys. It does not prohibit other keys at all. So a value like {a: "", b: "", c: "", d: 12345}
is valid. And Bar
is therefore a valid Partial<Foo>
.
Note that this is generally a desirable feature, since it allows interface
and class
extension:
interface X {
x: string;
}
interface Y extends X {
y: number;
}
Here, Y extends X
means that every Y
is an X
. This leads directly to the kind of excess property widening you're not happy about:
const y: Y = { x: "", y: 1 }
const x: X = y; // okay
There are some workarounds that approximate exact types, which might behave well enough for your purposes, but can all be circumvented because the compiler will always let you widen a value to a type that doesn't know about the offending keys.
My preferred workaround is to make myMethod()
generic in the Partial<Foo>
-like type T
that part
will be. You can constrain T
so that every known property in it needs to be a property from Foo
, and if there are any extra properties they must be never
. Like this:
mymethod<T extends { [K in keyof T]: K extends keyof Foo ? Foo[K] : never }>(
part: T
) {
console.log(part);
}
And test it out:
obj.mymethod({a: ""}) // okay
const bar: Bar = { a: "", d: "" };
obj.mymethod(bar); // error!
// Types of property 'd' are incompatible.
Hooray, it prevents a Bar
! And that's great if you hand objects to mymethod
which are known to have excess properties. But nothing will stop this:
const partialFoo: Partial<Foo> = bar; // valid assignment
obj.mymethod(partialFoo); // no error!
Here, partialFoo
is explicitly annotated as a Partial<Foo>
, and bar
is assigned to it. That succeeds because types in TypeScript are open, not exact. And the compiler has completely forgotten that partialFoo
came from a Bar
; it only sees it as Partial<Foo>
. So the keys known to the compiler are fine, and the offending key is unknown to the compiler, so it doesn't catch it.
At this point I'd say you might want to step back and consider working with TypeScript's type system instead of against it. If TypeScript treats object types as open, your code should do so as well, by explicitly operating only on the known keys. Imagine we have a function pluck()
which produces an object with only the keys we specify:
function pluck<T, K extends keyof T>(t: T, ...k: K[]): Pick<T, K> {
return k.reduce((a, k) => (k in t && (a[k] = t[k]), a), {} as Pick<T, K>);
}
And then make a version of mymethod()
that pluck
s the a
, b
, and c
properties from part
before operating on it:
mySafeMethod(part: Partial<Foo>) {
this.mymethod(pluck(part, "a", "b", "c"));
}
In this case, it's no longer a problem if you pass in a Bar
:
obj.mySafeMethod(bar); // { a: "" };
The runtime code expects that part
might have more properties than a
, b
, and c
, and explicitly ignores them instead of, for example, iterating all the existing keys with Object.keys()
or for..in
. This might be less idiomatic JavaScript, but it does make the compiler easier to deal with.
Okay, hope one of those ideas helps you; good luck!
Playground link to code
UPDATE
TypeScript doesn't really "get" exact types, especially unspecified generic ones like what you see in the implementation of mymethod
. So inside mymethod
the compiler doesn't know much about part
. There are ways to deal with this... one uglier way is to intersect the exact type with the open type so the compiler can at least know that T
is a Partial<Foo>
as well as the other stuff it can't verify:
mymethod<T extends Partial<Foo> &
{ [K in keyof T]: K extends keyof Foo ? Foo[K] : never }
>(
part: T
) {
console.log(part);
part.b?.toUpperCase(); // okay
}
Does that work for you?
Playground link to code