1

I have a fairly simple interface, Item, that is assigned to objects.

This interface specifies that each of these objects must have an itemName property set, but they can also have additional properties with dynamic names, if required.

I've attempted to implement this using

interface ItemProperty {
    foo: string;
    bar: string;
}
interface Item {
    itemName: string;
    [propertyName: string]: ItemProperty;
}

Now, obviously this throws the following error:

Property 'itemName' of type 'string' is not assignable to 'string' index type 'ItemProperty'.

As itemName is technically apart of ItemProperty, but it is a string and not an ItemProperty.

How do I override this, so that itemName can be set without needing to satisfy ItemProperty?

A final Item object might look like the following:

const item: Item = {
    itemName: "Item 1",
    dynamicProperty1: {
        foo: "foo",
        bar: "bar"
    }
};
GROVER.
  • 4,071
  • 2
  • 19
  • 66
  • You cannot. TS will look for type definition for a key in `Item` and if name is found, it will enforce strict type. If not, it will take the dynamic property definition. Maybe defining `itemName: string | ItemProperty` will solve but not sure if that is acceptable solution – Rajesh Jan 04 '23 at 03:44
  • Hmm, that's annoying. There's absolutely no way around this? Unfortunately, one of the APIs I'm using returns data like this and provides *zero* TypeScript definitions, so I'm forced to create them myself. @Rajesh – GROVER. Jan 04 '23 at 03:45
  • In that case, just set it as `string | ItemProperty`. That solves both case – Rajesh Jan 04 '23 at 03:48
  • There's no great way to represent this in TS, unfortunately. The "correct" thing to do is refactor so your known property doesn't reside in the same object with the dynamic properties. Otherwise there are just various workarounds, as described in the linked question. – jcalz Jan 04 '23 at 03:54
  • I would love to refactor. Unfortunately, I'm actually doing this for an API that doesn't currently have any type definitions that we use extensively. So I have no control over the spaghetti JSON they return. :( @jcalz – GROVER. Jan 04 '23 at 03:56
  • @jcalz The marked dupe does not addresses the issue. Issue is to have an option to override type with dynamic property – Rajesh Jan 04 '23 at 03:58
  • 1
    @Rajesh Anytime someone wants a structure like `{fixedProp: FixedType, [dynamicProps: string]: DynamicType}` where `FixedType` is not assignable to `DynamicType`, they run into all the issues linked in the other question, since TypeScript doesn't support this directly, and there's an outstanding feature request at [ms/TS#17867](https://github.com/microsoft/TypeScript/issues/17867). If this question is asking for something else, could you articulate exactly what? I'm not seeing a meaningful difference here. – jcalz Jan 04 '23 at 04:03

2 Answers2

1

Instead what you can do to fix this is to use a type union like the following:

type Item = {
    [propertyName: string]: ItemProperty
} | {itemName: string}

Here you accept a type of string or an object

interface ItemProperty {
    foo: string;
    bar: string;
}

type Item = {
    [propertyName: string]: ItemProperty
} | {itemName: string}


const a: Item = {
    itemName: 'something', // works
    a: {
        bar: 'a',
        foo: 'c'
    }, // works
    b: 'something else' // error: the key is not 'itemName'
}

Typescript Playground

Daniel Rodríguez Meza
  • 1,167
  • 1
  • 8
  • 17
  • This is fantastic. I had no idea you could do this. I'll check it out in a sec – thank you! – GROVER. Jan 04 '23 at 03:47
  • There is one issue with this. Try `const item = { itemName: '', foo: {foo: '', bar: ''}}`. This will throw error – Rajesh Jan 04 '23 at 03:49
  • Interesting. That is a bit annoying, isn't it? Is it the `itemName` that throws the error or the dynamic property? @Rajesh – GROVER. Jan 04 '23 at 03:50
  • @Rajesh I don't seem to get an error with the example that you provided, could you elaborate on what is the error? – Daniel Rodríguez Meza Jan 04 '23 at 03:51
  • OP is looking for a way to override type of a property and not entire type. So you need to respect other rules. You approach neglects the second rule – Rajesh Jan 04 '23 at 03:54
1

For an interface like:

interface Item {
   itemName: string;
    [propertyName: string]: ItemProperty;
}

TS will try to find a definition for property. If found, it will enforce it. If not, then it will go for dynamic definition.

A simple solution is to update the type definition of itemName to accept more than 1 type

interface Item {
   itemName: string | ItemProperty;
   [propertyName: string]: ItemProperty;
}
Rajesh
  • 24,354
  • 5
  • 48
  • 79
  • I feel as though this might be a little bit hacky. It may cause issues down the line if someone tries to target `itemName.foo` (which, according to type hinting, will be perfectly valid). – GROVER. Jan 04 '23 at 03:54
  • TS will warn you for possibility of it being string. This will enforce you to either typecast or add checks if its an object – Rajesh Jan 04 '23 at 03:55