0

Observe the following:

class Table<ValuesType extends DefaultTableValues = DefaultTableValues>{
    public values: ValuesType;
    constructor(initialValues:ValuesType) {
        this.values=initialValues;
    }

    public set(newValues:Partial<ValuesType>){
        this.values={
            ...this.values,
            ...newValues
        }
    }
}

class User<MoreValues extends UserValues = UserValues> extends Table<MoreValues>{
    constructor(values:MoreValues) {
        super(values);

        this.set({avatar:'some-string'});
        //ERROR - Argument of type {avatar:'some-string'} is not assignable to type Partial
    }
}
interface DefaultTableValues{
    id:string;
}
interface UserValues extends DefaultTableValues{
    avatar:string;
    username:string;
}

I get the following error with this.set: Argument of type {avatar:'some-string'} is not assignable to type Partial

If I change class User<MoreValues extends UserValues = UserValues> extends Table<MoreValues> to class User extends Table<UserValues> then it works, but I lose the ability to pass specific User types and extend the User class.

Shouldn't MoreValues always have the avatar:string property since it extends UserValues?

I have a feeling this is somehow related to this question but I can't figure out how to apply a fix to my case.

kmoney12
  • 4,413
  • 5
  • 37
  • 59

1 Answers1

1

You are correct in that the issue is the same in the question you linked. Per said question's accepted answer:

But the ability to subtype doesn't just apply to adding additional properties -- subtyping can include choosing a more restricted set of the domain of the properties themselves.

For example, assume the following interface:

interface RestrictedValues extends UserValues {
    avatar: "nonexistent"
}

This interface does extend UserValues, and is thus assignable to MoreValues. However, its avatar property can only have the value nonexistent, so the Partial<> which is set()'s first argument should only allow the value nonexistent, not any string as UserValues would imply. Take, for example, the following code:

class User<MoreValues extends UserValues = UserValues> extends Table<MoreValues>{
    constructor(values:MoreValues) {
        super(values);

        const thing1: RestrictedValues = { avatar: "nonexistent", id: "", username: "" };
        const thing2: MoreValues = thing1;
    }
}

In this, thing1 cannot be assigned to thing2, since it is not guaranteed that MoreValues will not instantiated with, say, { avatar: "" }, where the only accepted value is an empty string, to which "nonexistent" is not assignable. Therefore, TypeScript throws the error:

Type 'RestrictedValues' is not assignable to type 'MoreValues'.
  'RestrictedValues' is assignable to the constraint of type 'MoreValues', but 'MoreValues' could be instantiated with a different subtype of constraint 'UserValues'.

A simpler example of this is provided in this answer, using only booleans, which I recommend reading if you want to better understand the logic behind this behaviour.

That error is warning you, that your Generic Type P can't be assigned to {}, since the Generic Type P can be a more defined, or restricted, to a particular type that can conflict with the default value.

That means that the value {} can't satisfy all the possible Types that can be used by the Generic Type P.

Let's create another example with only booleans that should be easier to understand:

interface OnlyBoolIdentityInterface<T> {
  (arg: T): T;
}

function onlyBoolGeneric<T extends boolean>(arg: T = false): T {
  return arg;
}

if you define a Type that is more specific than a boolean for example:

type TrueType = true;

and if you specialised the function OnlyBoolIdentityInterface to only support true values like this:

const onlyTrueIdentity: OnlyBoolIdentityInterface<TrueType> = onlyBoolGeneric;

even if TrueType respects the constraint set by T extends boolean the default value arg: T = false is not a TrueType.

This is the situation is what the error is trying to convey to you.

So how can you fix this type of errors?

  1. Or you remove the default value
  2. Or T needs to extend the specialised type of the default param that on my example is false
  3. Or T can interfere directly with params that receive default params

For more context about this error message see the issue that suggested this error message https://github.com/Microsoft/TypeScript/issues/29049.

futur
  • 1,673
  • 5
  • 20