Given interfaces or classes A and B with a x1
field in common
interface A {
a1: number;
x1: number; // <<<<
}
interface B{
b1: number;
x1: number; // <<<<
}
And given the implementations a and b
let a: A = {a1: 1, x1: 1};
let b: B = {b1: 1, x1: 1};
Typescript allows this, even though b1 is not part of A:
let partialA: Partial<A> = b;
You can find the explaination of why this happens here: Why Partial accepts extra properties from another type?
Is an alternative to Partial to accept only fields from another type and nothing else (not requiring all the fields though)? Something like a StrictPartial
?
This has been causing a lot of problems in my code base as it simply does not detect that the wrong class is being passed as parameters to the functions.
What you really want is called exact types, where something like "Exact<Partial<A>>
" would prevent excess properties in all circumstances. But TypeScript does not directly support exact types (at least not as of TS3.5) so there's no good way to represent Exact<>
as a concrete type. You can simulate exact types as a generic constraint, meaning that suddenly everything that deals with them needs to become generic instead of concrete.
The only time where the type system treats types as exact is when it does excess property checks on "fresh object literals", but there are some edge cases where this doesn't happen. One of these edge cases is when your type is weak (no mandatory properties) like Partial<A>
, so we can't rely on excess property checks at all.
And in a comment you said you want a class whose constructor takes an argument of type Exact<Partial<A>>
. Something like
class Example {
constructor(public partialA: Exact<Partial<A>>) {} // doesn't compile
}
I'll show you how to get something like that, along with some caveats along the way.
Let's define the generic type alias
type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;
This takes a type T
and a candidate type U
that we want to ensure is "exactly T
". It returns a new type which is like T
but with extra never
-valued properties corresponding to the extra properties in U
. If we use this as a constraint on U
, like U extends Exactly<T, U>
, then we can guarantee that U
matches T
and has no extra properties.
For example, imagine that T
is {a: string}
and U
is {a: string, b: number}
. Then Exactly<T, U>
becomes equivalent to {a: string, b: never}
. Notice that U extends Exactly<T, U>
is false, since their b
properties are incompatible. The only way that U extends Exactly<T, U>
is true is if U extends T
but has no extra properties.
So we need a generic constructor, something like
class Example {
partialA: Partial<A>;
constructor<T extends Exactly<Partial<A>, T>>(partialA: T) { // doesn't compile
this.partialA = partialA;
}
}
But you can't do that because constructor functions cannot have their own type parameters inside class declarations. This is an unfortunate consequence of the interaction between generic classes and generic functions, so we will have to work around it.
Here are three ways to do it.
1: Make the class "unnecessarily generic". This makes the constructor generic as desired, but causes the concrete instances of this class to carry around a specified generic parameter:
class UnnecessarilyGeneric<T extends Exactly<Partial<A>, T>> {
partialA: Partial<A>;
constructor(partialA: T) {
this.partialA = partialA;
}
}
const gGood = new UnnecessarilyGeneric(a); // okay, but "UnnecessarilyGeneric<A>"
const gBad = new UnnecessarilyGeneric(b); // error!
// B is not assignable to {b1: never}
2: Hide the constructor and use a static function instead to create instances. This static function can be generic while the class is not:
class ConcreteButPrivateConstructor {
private constructor(public partialA: Partial<A>) {}
public static make<T extends Exactly<Partial<A>, T>>(partialA: T) {
return new ConcreteButPrivateConstructor(partialA);
}
}
const cGood = ConcreteButPrivateConstructor.make(a); // okay
const cBad = ConcreteButPrivateConstructor.make(b); // error!
// B is not assignable to {b1: never}
3: Make the class without the exact constraint, and give it a dummy name. Then use a type assertion to make a new class constructor from the old one which has the generic constructor signature you want:
class _ConcreteClassThatGetsRenamedAndAsserted {
constructor(public partialA: Partial<A>) {}
}
interface ConcreteRenamed extends _ConcreteClassThatGetsRenamedAndAsserted {}
const ConcreteRenamed = _ConcreteClassThatGetsRenamedAndAsserted as new <
T extends Exactly<Partial<A>, T>
>(
partialA: T
) => ConcreteRenamed;
const rGood = new ConcreteRenamed(a); // okay
const rBad = new ConcreteRenamed(b); // error!
// B is not assignable to {b1: never}
All of those should work to accept "exact" Partial<A>
instances and reject things with extra properties. Well, almost.
They reject parameters with known extra properties. The type system doesn't really have a good representation for exact types, so any object can have extra properties that the compiler doesn't know about. This is the essence of substitutability of subclasses for superclasses. If I can do class X {x: string}
and then class Y extends X {y: string}
, then every instance of Y
is also an instance of X
, even though X
doesn't know anything about the y
property.
So you can always widen an object type to make the compiler forget about properties, and that's valid: (Excess property checking tends to make this more difficult, in some cases, but not here)
const smuggledOut: Partial<A> = b; // no error
We know that compiles, and nothing I do can change that. And that means that even with the implementations above, you can still pass a B
in:
const oops = new ConcreteRenamed(smuggledOut); // accepted
The only way to prevent that is with some kind of runtime check (by examining Object.keys(smuggledOut)
. So it's a good idea to build such a check into your class constructor if it's really damaging to accept something with extra properties. Or, you could build your class in such a way that it will silently discard extra properties without being damaged by them. Either way, the above class definitions are about as far as the type system can be pushed in the direction of exact types, at least for now.
Hope that helps; good luck!
Link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With