Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Types of property 'X' are incompatible" but it is not

Tags:

typescript

I tried to create an object of an interface with properties initialized from another object like that:

id: data.reference.id

The properties are compatible, but typescript compiler throws an error. I don't understand why and how to avoid this error.

(a link to test the code can be found after)

// INTERFACES AND TYPES
type CategoryOne = 1
type CategoryTwo = 2
type Category = CategoryOne | CategoryTwo

interface ReferenceOne {
    id: string,
    type: CategoryOne
}

interface ReferenceTwo {
    id: string,
    type: CategoryTwo
}

type Reference = ReferenceOne |  ReferenceTwo

// DIRECT INIT
let reference_OK: Reference = { // works here
    id: '1',
    type: 1
}

// INIT FROM DATA
interface Data {
    reference: {
        id: string,
        type: Category
    }
}

let data: Data = {
    reference: {
        id: '1',
        type: 1
    }
}

let reference_KO: Reference = { // <--- error here
    id: data.reference.id,      // but compatible property
    type: data.reference.type   // but compatible property
}

let param: Category = data.reference.type // == 1

let reference_KO2: Reference = { // <--- error here
    id: data.reference.id,       // but compatible property
    type: param                  // but compatible property
}

param = 1 // == data.reference.type, same variable

let reference_OK2: Reference = { // <--- no error here
    id: data.reference.id,
    type: param
}

this code in Typescript playground

[update] I added the case when a new reference is created from a variable (reference_KO2 - variable initialized from a property of data - and reference_OK2 - same variable initialized with a constant)

two behaviors with the same variable of a compatible type !

like image 403
Frédéric Mascaro Avatar asked Jul 05 '17 16:07

Frédéric Mascaro


Video Answer


1 Answers

While the code appears to be semantically correct, the compiler will disagree because Data does not retain additional information about the category underneath: it only knows that the field type is a Category.

So, we know that reference_ok can be assigned either a ReferenceOne or a ReferenceTwo. ReferenceOne requires type to be a CategoryOne, whereas ReferenceTwo requires type to be a CategoryTwo. Neither of these work, since data.reference.type is a generalized Category. We could reproduce the same effect with less code, by "upcasting" the value on purpose:

let r: Reference = {
  id: '1',
  type: 1 as Category,
}

There are at least two ways to avoid this situation. One of them is to redefine Reference as an interface, which basically tells the compiler that we don't need to know the category in compile time at this point.

interface Reference {
    id: string,
    type: Category,
}

The most appealing solution, however, is with generics. We can augment Data to also contain the type of the category:

interface Data<C extends Category> {
    reference: {
        id: string,
        type: C
    }
}

At this point, this code works:

let data: Data<CategoryOne> = {
    reference: {
        id: '1',
        type: 1
    }
}

let reference_KO: Reference = {
    id: data.reference.id,
    type: data.reference.type
}

The same treatment can be applied to Reference, if so you wish.

like image 78
E_net4 stands with Ukraine Avatar answered Sep 17 '22 00:09

E_net4 stands with Ukraine