Is there an alternative to Partial to accept only fields from another type and nothing else?
Asked Answered
G

2

9

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.

Global answered 15/6, 2019 at 1:13 Comment(5)
You want exact types which are not directly supported in TypeScript. You can simulate them via a helper function if you want.Maxima
Something like thisMaxima
Let me know if you want that fleshed out into an answer. I'm almost sure there are other questions about how to get exact-like behavior in TypeScript, so I might end up closing this as a duplicate.Maxima
Hi @jcalz, thank you but that does not solve my problem. The issue is that I want to accept a "Partial" like type in the constructor of my classes, and I would like to check that whoever is instantiating the class is doing so passing a valid type as parameter.Global
You might want to include your use case in the question, along with a minimal reproducible example so that someone can tell what constitutes a solution to your problem.Maxima
M
15

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

Maxima answered 16/6, 2019 at 17:0 Comment(1)
thank you @Maxima It is a pitty that typescript does not support that... I think I can live with the second approach for now. I will post the use case example there as you suggestedGlobal
F
0

Coming late, but this worked for me:

export type StrictPartial<Subset, Original> = {
[K in keyof Subset]:
    K extends keyof Original ?
        StrictPartial<Subset[K], Original[K]> 
    : Subset extends { [key: string]: Original } ?
        StrictPartial<Subset[K], Original>:never
};


function fn<T extends StrictPartial<T, {a:number,b:number}>>() {

}

fn<{a:number}>() //works
fn<{a:number,c:number}> //type error
Faye answered 8/2, 2023 at 1:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.