h-Reser
h-Reser

Reputation: 177

Creating a type which defines keys of an object containing a specific nested property

I have an interesting case where it would be very useful to dynamically define types from a central JSON data store. Let me explain.

I have a json file that contains a list of brands

// brands.json
{
    "Airbus": {
        "keywords": ["A320", "A380"],
        "type": ["planes"]
    },
    "Jaguar": {
        "keywords": ["fiesta"],
        "origin": "UK",
        "type": ["cars", "glasses"]
    },
    "Nissan": {
        "keywords": ["qashqai"],
        "type": ["cars"]
    }
}

Then I have my type definitions:

import brands from "./brands.json

type BrandNames= keyof typeof brands ;

type Brands = {
    [P in BrandNames]: {
        keywords: string[];
        origin?: string;
        type: string[];
    }
};

I have created a type CompanieNames which is automatically generated and is equal to "Airbus" | "Jaguar" | "Nissan".

So far so good...

Now, I want to create a type CarBrands, which will be equal to "Jaguar" | "Nissan".

Doing type CarBrands = Exclude<CompanieNames, "Airbus">; would work, but this is not dynamic.

So instead, I need to filter out all keys from Brands which have a nested type property that doesn't contain the string "cars".

Is it possible to do that?

Upvotes: 1

Views: 57

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250376

If the types of the string literal types were preserved, we could do what you want (ie extract only the brands that have car in the types field). Typescript will however widen the types of the values in the types array to string so this information is lost to us when we actually look at the type of the json object.

Just for fun, using the as const in 3.4 (unreleased yet) to preserve all string literal types, this is what the solution would look like:

let data = {
    "Airbus": {
        "keywords": ["A320", "A380"],
        "type": ["planes"]
    },
    "Jaguar": {
        "keywords": ["fiesta"],
        "origin": "UK",
        "type": ["cars", "glasses"]
    },
    "Nissan": {
        "keywords": ["qashqai"],
        "type": ["cars"]
    }
} as const;

type Data = typeof data;
type CarBrands = {
    [P in keyof Data]: 'cars' extends Data[P]['type'][number] ? P : never
}[keyof Data]

Again the above does not work for imported json modules because typescript will not preserve the literal types in the string arrays and there is no way to tell it to do so at the time of writing.

Upvotes: 2

Related Questions