#Creating an interface for a type with one dynamic property name

38 messages · Page 1 of 1 (latest)

timber mountain
#

I have multiple arrays of objects coming from an API
They are all the same except they have different property name for an id to match with types

f.e.

  id: 1,
  value: true,
  name: 'Something1',
  carTypeId: 1
}

const plane = {
  id: 1,
  value: true,
  name: 'Something2',
  planeTypeId: 1
}```

So I need a Type object for this, a generic probablt
And then I need to be able to send an array of one these types into a function and be able to access the TypeId property
So I'm assuming a Generic function as well, but that doesn't seem to work.

Here is playground I tried
sacred dirgeBOT
#
ingovals#0

Preview:```ts
...
const car = {
id: 1,
value: true,
name: "Something1",
carTypeId: 1,
}

const plane = {
id: 1,
value: true,
name: "Something2",
planeTypeId: 1,
}
...```

timber mountain
#

This did not work either

function ConcatSpecificVehiclesWithTypes(
  name: string,
  vehicles: Vehicle<typeof name>[],
  types: VehicleType[],
) {
  return vehicles.map(v => {
    const type = types.find(t => t.id === v[name])

    return {
      ...v,
      type: type?.typename
    }
  })
}

let result = ConcatSpecificVehiclesWithTypes('carTypeId' as const, [v1], CarTypes)
spring socket
#

who's to say that extra key isn't value or name or id, meaning there's no extra key at all?
this is an api that's really hard to design around because it gives no info about what info it has

#

the error in your playground is because you're trying to use TypePropertyName, a type, as a value. ts types don't exist at runtime

timber mountain
#

Yeah hence my other attempt, which does actually work in JS

spring socket
timber mountain
#

Typescript is actually smart enough to grab the extraKey as value, name or id, type not assingeable to never

spring socket
#

not sure what you mean by that

timber mountain
#

const v4 : Vehicle<'value'> = {id: 1, value: true, name: ''}

spring socket
#

passing id/name/value is absolutely allowed, they are just strings

spring socket
#

the type overlaps there

timber mountain
#

ah, I see, yeah it has to be a number

spring socket
#

arbitrary keys in an otherwise well-defined object is hell to deal with in ts

#

!:index%

sacred dirgeBOT
#
gerrit0#0
`!gerrit0:index-signatures`:

Index signatures apply to all members of the type that match the signature. Therefore, the following type is invalid, even though TypeScript does not detect it until you actually try to construct a value which conforms to the type.

type Foo = { a: 1 } & { [x: string]: 2 }

TypeScript does not have a way to specify "everything besides defined keys" since it does not have negated types. This pattern cannot be expressed in TypeScript today. A more TS-friendly version of it puts all of the extra properties on a dedicated key:

type Foo = { a: 1; extra: { [x: string]: 2 } }

negated types: https://github.com/microsoft/TypeScript/pull/29317

spring socket
#

is this api your own, or...?

timber mountain
#

nope 😦

spring socket
#

rip

timber mountain
#
function ConcatSpecificVehiclesWithTypes<TypePropertyName extends string>(
  name: TypePropertyName,
  vehicles: Vehicle<TypePropertyName>[],
  types: VehicleType[],
) {
  return vehicles.map(v => {
    const type = types.find(t => t.id === v[name])

    return {
      ...v,
      type: type?.typename
    }
  })
}

let result = ConcatSpecificVehiclesWithTypes('carTypeId', [v1], CarTypes)

This does work, but as you pointed out it is not 100% safe. But it should be safe in praxis I guess

#

I mean its more safe then standard JS

bleak jasper
#

are the ID keys totally dynamic? like could they be anything at runtime? or could you know the entire set of possible keys ahead of time?

#

or even if you can't know them all ahead of time, do they all follow a pattern? like `${string}TypeId` in your examples

timber mountain
#

I know them all compile time, it is a limited, but somewhat large set

bleak jasper
#

then i'd suggest making a union type of them all and use that rather than string

#
type IdPropertyName = 'carTypeId' | 'planeTypeId' // | ...
#

then instead of stuff like TypePropertyName extends string, do TypePropertyName extends IdPropertyName

timber mountain
#

Yeah, that is reasonable

bleak jasper
#

you could probably even make your Vehicle type be non-generic and instead just use a union of all the possibilities

timber mountain
#

Thanks @bleak jasper

#

!resolved

timber mountain
#

@bleak jasper I'm curious since you mentioned it. If it would be dynamic, and it followed a pattern, how could you use that. In case it comes in handy later.

bleak jasper
#

here's a small example of what i was thinking about:

sacred dirgeBOT
#
type Busted = Record<string, string> & { something: number }

const busted1: Busted = { something: 42 }
//    ^^^^^^^
// Type '{ something: number; }' is not assignable to type 'Busted'.
//   Type '{ something: number; }' is not assignable to type 'Record<string, string>'.
//     Property 'something' is incompatible with index signature.
//       Type 'number' is not assignable to type 'string'.
const busted2: Busted = { something: 'umm' }
//                        ^^^^^^^^^
// Type 'string' is not assignable to type 'number'.


type Works = Record<`${string}TypeId`, string> & { something: number }

const works1: Works = { something: 42 }
const works2: Works = { someTypeId: 'abc', anotherTypeId: 'xyz', something: 42 }
bleak jasper
#

Works is an actually-usable type because the compiler tell that something doesn't overlap with `${string}TypeId`, so there's no conflicting requirements for its type

#

neither this solution or a union-based one will automatically solve the problem of having two different ID properties in the same value. you'll have to decide how you want the runtime to pick which ID property to prefer, or support multiple if you can

#

(though actually i can imagine a way to deal with that in a union setup. let me know if this is an actual problem for you and you want to discuss more)