0

typescript playground

So I'm trying to create a more general function which can help me transform data from an object instead something else.

However, with my current implementation, I'm getting the typescript error

Element implicitly has an 'any' type because expression of type '"channel" | "id" | "name"' can't be used to index type '{ id: string; name: string; source: string; }'.
  Property 'channel' does not exist on type '{ id: string; name: string; source: string; }'.

Since the keys may not be matching up, but I am unsure on how to make it into a proper generic function.

Code:

type channelCreateChanges = {
  change_from : {}
  change_to : {
    id : string,
    name : string,
    source : string,
  },
  datetime : string,
  operation : "create",
  entity : "channel",
}
type channelUpdateChanges = {
  change_from : {
    id : string,
    name : string,
    source : string,
  }
  change_to : {
    id : string,
    name : string,
    source : string,
  },
  datetime : string,
  operation : "update",
  entity : "channel",
}
type programCreateChanges = {
  change_from : {
  }
  change_to : {
    channel : string,
    id : string,
    name : string,
    origin : string,
  },
  datetime : string,
  operation : "create",
  entity : "program",
}
type programDeleteChanges = {
  change_from : {
    channel : string,
    id : string,
    name : string,
    origin : string,
  }
  change_to : {
  },
  datetime : string,
  operation : "delete",
  entity : "program",
}

type dataChangeRow = {
  from : string,
  to : string,
  datetime : string,
  operation : string,
  entity : string
}

const programKeys = [
  "id",
  "channel"
] as const;
const channelKeys = [
  "id",
  "name",
] as const;

type allChanges = channelCreateChanges | channelUpdateChanges | programCreateChanges | programDeleteChanges
type allKeys = typeof programKeys | typeof channelKeys

function ANY_CHANGE_LOG_TO_ROWS (data : allChanges, keys : allKeys) : dataChangeRow[] {
    const rows = [] as dataChangeRow[];
    for (const key of keys) {
      const row = {
        datetime : data.datetime,
        entity : data.entity,
        operation : data.operation
      } as dataChangeRow;

      if (data.operation === "create") {
        row.from = `-`;
        row.to = `${key}: ${data.change_to[key]}`;
            }
      else if (data.operation === "update") {
        row.from = `${key}: ${data.change_from[key]}`;
        row.to = `${key}: ${data.change_to[key]}`;
      }
      else if (data.operation === "delete") {
        row.from = `${key}: ${data.change_from[key]}`;
        row.to = `-`;
      }
            rows.push(row);
    }
    return rows;
}
2
  • I don't see any generics in your 100 lines of code. As for the question itself: you're trying to access properties on objects that might not have those properties defined. E.g., the key could be channel, but channelCreateChanges.change_to does not have a channel property. Commented Jan 17, 2023 at 7:35
  • @RobbyCornelissen Yes, that's kind of the problem. So I'm wondering what the solution could be, because the implementation is basically the same across both categories Commented Jan 17, 2023 at 11:07

2 Answers 2

1

As Robby notice to get this working, you need that Channel have 'channel' property.

I propose to forget about specify keys for specific logging each object type. (probably not what you want)

Here is a real generic implementation :

type Channel = { id : string; name : string; source : string }
type Program = { id : string; name : string; origin : string; channel : string }

type OperationType = "create" | "update" | "delete";

type Operation<ENTITY, OP extends OperationType> = {
  change_from : OP extends "update" | "delete" ?  ENTITY : undefined;
  change_to : OP extends "update" | "create" ? ENTITY : undefined;
  datetime : string,
  operation : OP,
  entity : ENTITY extends Channel ? 'Channel' : ENTITY extends  Program ? 'Program' : 'unknown';
 } 

type channelCreateChanges = Operation<Channel,"create">
type channelUpdateChanges = Operation<Channel,"update">
type programCreateChanges = Operation<Program,"create">
type programDeleteChanges = Operation<Program,"delete">


type dataChangeRow = {
  from : string,
  to : string,
  datetime : string,
  operation : string,
  entity : string
}

type allChanges = channelCreateChanges | channelUpdateChanges | programCreateChanges | programDeleteChanges

function ANY_CHANGE_LOG_TO_ROWS (allOperations : Array<allChanges>) : dataChangeRow[] {
    const rows = allOperations.map((ope) => {
        const row : dataChangeRow = {
            from : ope.change_from ? JSON.stringify(ope.change_from) : '-',
            to : ope.change_to ? JSON.stringify(ope.change_to) : '-',
            datetime : ope.datetime,
            entity : ope.entity,
            operation : ope.operation
         } ;
         return row;
     })
  
    return rows;
}

If you really needs to log speciifc key, please notice that you can get these :

type AllKeys = keyof Channel | keyof Program; // "id" | "name" | "source" | "origin" | "channel"
type CommonKeys = keyof Channel & keyof Program; // "id" | "name" 

You can se that you use Allkeys, but what you really want is CommonKey, to address any of Channel or Program by indexing.

If you need 'channel' property you have to manage so that CommonKeys equals to "id" | "name" | "channel"

For example you can add {channel:undefined} property to Channel type.

Sign up to request clarification or add additional context in comments.

1 Comment

The specific keys for logging are actually desired, because not all properties within the returned api data is meant to be displayed. Also please bear in mind that there may be more types, not just program and channel. Your answer doesn't seem to be very good for more variations
0

OK !

Now i have understood what you want, sorry for the mistake.

I let you see details and test in link below, but in summary here is your function implementation :

const ANY_CHANGE_LOG_TO_ROWS = <ENTITY, OP extends OperationType, K extends keyof ENTITY>(ope : Operation<ENTITY,OP>, keyToSerialise : Array<AllKeys>) : dataChangeRow[] => {
{
    const rows = Array<dataChangeRow>();
    
    const entityKeys = (ope.change_to === undefined ? Object.keys(ope.change_from!) : Object.keys(ope.change_to!) )  as Array<K>;

    for(const key of entityKeys)
        if(keyToSerialise.includes(key as AllKeys)) {
          const row : dataChangeRow = {
            from : ope.change_from ? (key as string + ":" + (ope.change_from as ENTITY)[key]): '-',
            to : ope.change_to ?  (key as string + ":" + (ope.change_to as ENTITY)[key]): '-',
            datetime : ope.datetime,
            operation : ope.operation,
            entity : ope.entity,
          } ;
          rows.push(row)
        }
    return rows;
  }
}

There is many cast, that i guess would be valid.

please see this playground to more detailed code, with comments on tricky parts.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.