1

I do not understand the error in the following Minimal Reproducible Example

interface Animal {
    properties: Array<{ foo: number }>
}

type Cat = Animal & {
    properties: Array<{ foo: number, bar: number }>
}

let cat : Cat = {} as any

cat.properties[0].bar
cat.properties.forEach(prop => prop.bar)

//                             ^^^^^^^^
// Property 'bar' does not exist on type '{ foo: number; }'.

Link to Playground

I expect the two last lines to either fail or succeed the type checking.

Why does cat.properties[0].bar pass the type checking, while the last line doesn't? Do I really need a cast to make the last line work?

I'd like to merely add some properties to the Animal interface and its children.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Bilow
  • 2,194
  • 1
  • 19
  • 34
  • "I'd like to merely add some properties to the Animal interface and its children": well, that's not what your code is trying to do. Your code is adding some property to the (anonymous) type of `properties` items. So what do you really want to extend? The `Animal` interface or the `Animal.properties` type? – lbsn Jul 26 '21 at 09:34
  • 1
    As for the reason why those two lines of code behave differently, the answer is probably [here](https://github.com/microsoft/TypeScript/issues/11961#issuecomment-257444466). Your `properties` type ends up being defined as something like this: `T[] & U[]`. – lbsn Jul 26 '21 at 09:47

3 Answers3

3

My guess is that because the same "properties" name is used. Intersection behave differently than extends with that. Typescript doesn't know wich one to use and he pick one of them. Here they said conflict are handled differently https://www.typescriptlang.org/docs/handbook/2/objects.html#interfaces-vs-intersections which seems to be exactly your case. You may take a look at something like this if you want to override properties https://stackoverflow.com/a/54774013/10691359

On the contrary if you extend your Animal, then the overload propertie detection function very well and you'll have access to bar

interface CatA extends Animal {
    properties: Array<{ foo: number, bar: number }>
}

let catA : CatA = {} as any;

catA.properties[0].bar
catA.properties.forEach(prop => prop.bar)
Pilpo
  • 1,236
  • 1
  • 7
  • 18
1

Since @Pilpo was first, I think his answer should be accepted. My answer just provides more explanation.

Let's take a look on Array type definition:

interface ArrayConstructor {
    new(arrayLength?: number): any[];
    new <T>(arrayLength: number): T[];
    new <T>(...items: T[]): T[];
    (arrayLength?: number): any[];
    <T>(arrayLength: number): T[];
    <T>(...items: T[]): T[]; // <---------we are interested only in this function signature
    isArray(arg: any): arg is any[];
    readonly prototype: any[];
}

declare var Array: ArrayConstructor;

Intersection of functions produces function overloads.


type Foo=(a:number)=>string
type Bar=(a:string)=>number

type Overload=Foo & Bar
declare var fn:Overload


fn(2) // string
fn('str') // number

Same you have in your example: overloads

0

Previous answer is correct, but I'd like to add that it's more clear if you declare types for different kind of properties. Also export everything: Let's say you want to make a component for editing a property of each animal type. If you can import the property type needed, you can be type-safe there too.

Also please never use as any or your code may break in so many ways. If something is optional, mark it as optional like properties?: AnimalProperty[].

export interface AnimalProperty {
  foo: number;
}

export interface CatProperty extends AnimalProperty {
  bar: number;
}

export interface Animal {
  properties: AnimalProperty[];
}

export interface Cat extends Animal {
  properties: CatProperty[];
}

const cat: Cat = { properties: [] }; // Don't use `as any`

funkizer
  • 4,626
  • 1
  • 18
  • 20