1

Consider the file data.json:

{
   "au": {
      "name": "Australia",
      "market_id": "111172",
      "language_code": "en_AU"
   },
   "br": {
      "name": "Brazil",
      "market_id": "526970",
      "language_code": "pt_BR"
   },
   "ch": {
      "name": "China",
      "market_id": "3240",
      "language_code": "zh_CN"
   },
   "de": {
      "name": "Germany",
      "market_id": "4",
      "language_code": "de_DE"
   }
    ...many more
}

In Typescript, we can import that file as a json module ("resolveJsonModule": true in tsconfig.json):

import lookup from './data.json'

The imported module has type information:

type LookupType = typeof lookup

LookupType = {
    au: {
        name: string;
        market_id: string;
        language_code: string;
    };
    br: {
        name: string;
        market_id: string;
        language_code: string;
    };
    ch: {
        name: string;
        market_id: string;
        language_code: string;
    };
    de: {
        name: string;
        market_id: string;
        language_code: string;
    };
    ...
}

With that type information in place we can do something like this:

type CountryCodes = keyof LookupType

CountryCodes = "au" | "br" | "ch" | "de" | ...

What I would like to do however, is to extract a type that represents all possible values for a given nested field in the json.

For example:

type MarketIds = LookupType[keyof LookupType]['market_id']

MarketIds = string // I'd like "111172" | "526970" | "3240" | "4" ...

The issue being that Typescript sees LookupType[keyof LookupType] as a type erased union:

LookupType[keyof LookupType] = {
    name: string;
    market_id: string;
    language_code: string;
} | {
    name: string;
    market_id: string;
    language_code: string;
} | {
    name: string;
    market_id: string;
    language_code: string;
} | {
    name: string;
    market_id: string;
    language_code: string;
} ...

I understand this might simply be a hard limitation of Typescript. - Understandably, given that deeply nested typed json data would blow up the compiler, I guess.

However, since I've not found any definite information in the docs about json modules only supporting one level of type information, I have hope that there might be some way to achieve the goal outlined above.

3
  • It's not really about "levels" so much as TypeScript inferring key types as literals but property types as non-literals. What you really want here is some sort of const assertion for json module imports, which is a requested feature microsoft/TypeScript#32063 but has not been implemented. I don't know of any way to deal with this other than the clunky build step workarounds in that issue. Commented Sep 24, 2021 at 14:50
  • I'll probably write up an answer saying as much as the above, unless you think I'm missing something about your question. Let me know. Commented Sep 24, 2021 at 14:53
  • Ah good to know! Thanks for the info. If you provide an answer linking to the TypeScript issue on GitHub I'm happy to accept it :). Commented Sep 24, 2021 at 23:12

1 Answer 1

3

When an initializing value is assigned to a variable with no type annotation, the compiler uses some heuristics to infer a type for the variable. For object literals, the compiler infers the property keys as literal types like "au" or "name" so it remembers and enforces key names to be unchanging. But it infers the property values more widely like string or number so it does not generally remember or enforce that the value stay exactly what it was initially. After all, object properties often change in value.

So if you do this:

const lookup = {
   "au": {
      "name": "Australia",
      "market_id": "111172",
      "language_code": "en_AU"
   },
   "br": {
      "name": "Brazil",
      "market_id": "526970",
      "language_code": "pt_BR"
   }
}

you get this:

/* const lookup: {
    au: {
        name: string;
        market_id: string;
        language_code: string;
    };
    br: {
        name: string;
        market_id: string;
        language_code: string;
    };
} */

Of course, this isn't always what is desired. Sometimes people use initializing values which are really not supposed to change at all; in such cases, it would be nice if the compiler could remember as much as possible about the initializing value and not allow anyone to change it afterward.

If you are writing such an initializer in TypeScript, you can use a const assertion to achieve this effect:

const lookup = {
    "au": {
        "name": "Australia",
        "market_id": "111172",
        "language_code": "en_AU"
    },
    "br": {
        "name": "Brazil",
        "market_id": "526970",
        "language_code": "pt_BR"
    }
} as const;

which produces this type:

/* const lookup: {
    readonly au: {
        readonly name: "Australia";
        readonly market_id: "111172";
        readonly language_code: "en_AU";
    };
    readonly br: {
        readonly name: "Brazil";
        readonly market_id: "526970";
        readonly language_code: "pt_BR";
    };
} */

Here, each property is now considered to be readonly, and each value is considered to be of a string literal type. Now the compiler knows quite a lot about lookup, and from this you could use typeof lookup to extract all the unions-of-string-literals you want.


Unfortunately, const assertions are (as of TypeScript 4.4) only available when the initializer in question is in TypeScript code.

The --resolveJsonModule compiler flag allows you to import JSON files as if they were modules, but there's no opportunity for you to put an as const anywhere. So such imported JSON files get types corresponding to the regular heuristics expecting properties to change.

There is a feature request at microsoft/TypeScript#32063 asking for support to import as const. It is currently open and marked as "Awaiting more feedback", so if you want to see this implemented you might want to go to that issue, give it a 👍 and describe your use case and why it's compelling.

Until and unless it gets implemented, I'm not sure there's a great solution for you. You could add an extra build step that produces a TypeScript file or a declaration file from your JSON file and imports the type from that; see this comment and other related comments in that issue. This is similar to saying "forget about JSON and just use TypeScript instead", so it's not ideal. Still it could be better than nothing. 🤷‍♂️

Playground link to code

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

1 Comment

Awesome answer. Thanks for all the detail. I used the information in this comment to try out typed imports with explicit declaration files, but it felt too cumbersome for me. I'll wait until Typescript supports this in a more elegant way - fortunately, I have control over the .json files and can restructure them as needed.

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.