0

I'm trying to create typing for an object that has a structure like this:

{
  "1bcc10d3-425e-4a91-9a90-d7f2969986f9": {
    result: {
      key: "value"
    }
  },
  another_key: ["test", "12345"]
}

In the example above, "1bcc10d3-425e-4a91-9a90-d7f2969986f9" is an arbitrary UUID that is being used as a key. My object can have one or more such records, indexed by a UUID, and it also has a few other keys of various types.

I have tried a few different representations, none of which have worked. The most obvious would be something like this:

interface MyResultType {
  [uuid: string]: {
    result: {
      key: string;
    }
  };
  another_key: string[];
}

That doesn't work however, as I get the following error:

Property 'another_key' of type 'string[]' is not assignable to 'string' index type '{ result: { key: string; }; }'.ts(2411)

My understanding is that the problem here is that [uuid: string] matches any key of type string, so it's trying to assign another_key to the {result: {key: string;}} type.

So I tried using an intersection type:

type MyResultType = {
  [uuid: string]: {
    result: {
      key: string;
    };
  };
} & {
  another_key: string[];
};

This time, the declaration works fine, but it fails when I try to use it:

const result: MyResultType = {
  '1bcc10d3-425e-4a91-9a90-d7f2969986f9': {
    result: {
      key: 'value',
    },
  },
  another_key: ['test', '12345'],
};

Results in this error:

Type '{ '1bcc10d3-425e-4a91-9a90-d7f2969986f9': { result: { key: string; }; }; another_key: string[]; }' is not assignable to type 'MyResultType'.
  Type '{ '1bcc10d3-425e-4a91-9a90-d7f2969986f9': { result: { key: string; }; }; another_key: string[]; }' is not assignable to type '{ [uuid: string]: { result: any; }; }'.
    Property 'another_key' is incompatible with index signature.
      Property 'result' is missing in type 'string[]' but required in type '{ result: any; }'.ts(2322)

I tried rephrasing it like this:

type MyResultType = {
  [uuid: Omit<string, 'another_key'>]: {
    result: any;
  };
} & {
  another_key: string[];
};

But that still doesn't work:

An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.ts(1268)

Is there any way to do what I'm trying to do?

jonneve
  • 555
  • 4
  • 16
  • Ideally your object type is has all unknown keys or all known keys. Having an object type with both is a path that leads to pain. `{ items: { [uuid: string]: unknown }, another_key: string }` will be your salvation if you can control the source this data is coming from. – Alex Wayne Jan 12 '23 at 20:26
  • The correct UUIDv4 union looks like [this](https://tsplay.dev/mL51ZW), but it is "too complex to represent" (TS Error 2590). Refs: [UUID format](https://en.wikipedia.org/wiki/Universally_unique_identifier#Format), [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). You might be interested in [branded](https://github.com/microsoft/TypeScript/wiki/FAQ#can-i-make-a-type-alias-nominal) strings — e.g. https://stackoverflow.com/a/70223085/438273. – jsejcksn Jan 12 '23 at 21:29

2 Answers2

0

Just define index type as an intersection of all possible value types

interface Field {
    value: unknown
    lastUpdateAt: number
}

interface User {
    [key: string]: Field | string[]
    extra: string[]
}

const person: User = {
    firstName: {
        value: 'Billy',
        lastUpdateAt: 6,
    },
    lastName: {
        value: 'Herrington',
        lastUpdateAt: 9,
    },
    extra: ['sex']
}
0

Based on feedback from answers to this other question, I tried the following:


interface Result {
  another_key: any[];
}
interface ExtendedResult {
  result: {
    key: string;
  };
}
export type PolicyCheckResult = {
  [y: string]: ExtendedResult;
} & Result;

type VerifyT<T> = {
  [K in keyof T]: K extends keyof Result ? Result[K] : ExtendedResult;
};
export const asPolicyCheckResult = <T extends VerifyT<T>>(t: T) => t;
asPolicyCheckResult({
  test: {
    result: {
      key: 'value'
    },
  },
  another_key: [],
});

That works, but unfortunately, it doesn't solve my problem, because in my case, the key we're indexing on is unknown, so I would have to write something like this, where test is a variable of type string:

asPolicyCheckResult({
  [test]: {
    result: {
      key: 'value'
    },
  },
  another_key: [],
});

And that fails again:

Argument of type '{ [x: string]: string | never[] | { result: { final_action: string; header: string[]; remediation: [{ suggestions: [{ message: string; preferred: true; }]; triggerIds: string[]; }]; row_count: number; rows: [(string | boolean)[], (string | boolean)[]]; }; }; policy_data: never[]; policy_name: string; whitelist_data:...' is not assignable to parameter of type 'VerifyT<{ [x: string]: string | never[] | { result: { final_action: string; header: string[]; remediation: { suggestions: { message: string; preferred: boolean; }[]; triggerIds: string[]; }[]; row_count: number; rows: (string | boolean)[][]; }; }; policy_data: never[]; policy_name: string; whitelist_data: never[]; w...'.
  'string' index signatures are incompatible.
    Type 'string | never[] | { result: { final_action: string; header: string[]; remediation: [{ suggestions: [{ message: string; preferred: true; }]; triggerIds: string[]; }]; row_count: number; rows: [(string | boolean)[], (string | boolean)[]]; }; }' is not assignable to type 'ExtendedResult'.
      Type 'string' is not assignable to type 'ExtendedResult'.ts(2345)

I realize that the real solution here would be to restructure the data to avoid mixing unknown types with known keys, but that's unfortunately not possible for me because I'm trying to add types to an existing API that is currently untyped and that I have no control over. So I need some way to provide information about the expected shape of what I get from the API...

So for now, my solution is to keep the types as above but making the additional properties (another_key) optional. I then use my literal without those extra properties, and add them back in using Object.assign. It's not pretty, but it's better than having no typing at all.

jonneve
  • 555
  • 4
  • 16
  • 2
    This sounds like it could be an opportunity to wrap your API call with a sort of "filtering" interface that allows you to get reliable types out of it. It wouldn't necessarily be pretty on the inside, but I've found with TypeScript that sometimes the pursuit of perfect types is a wild goose chase. – Isaac Corbrey Jan 12 '23 at 21:27