Home > Mobile >  Typescript partial key mapping
Typescript partial key mapping

Time:05-27

I'm trying to create a generic type that would map the keys using template literals. In general i just want to created a nested type from the list of commaseparated keys:

type TFoobar = "address" | "year" | "owner.age" | "owner.address.street";

const foobar: DotToType<TFooBar> = {
    address: true,
    owner: {
        age: true,
        address: {
            street: true
        }
    }
}

I have tried implementing it like so:

type DotToType<keys extends string> = {
    [Key in keys]: Key extends `${infer Parent}.${infer Leaf}`
        ? {[key in Parent]: DotToType<Leaf>}
        : {[k in Key]: boolean}
}[keys];

And while it does have a proper typing, it makes the properties optional (at least one is required, but there is nothing that enforces they are all present)

const foobar: DotToType<TFooBar> = {
    address: true,
}; //valid, shouldnt be

const foobar: DotToType<TFooBar> = {
    owner: {
        age: true
    },
}; //valid, shouldnt be

const foobar: DotToType<TFooBar> = {
    address: true,
    year: false,
    owner: {
        age: true,
        address: {
            street: true
        }
    }
}; //valid

I also tried doing it like this:

type DotToType<keys extends string> = {
    [Key in keys as Key extends `${infer Parent}.${infer Leaf}` ? Parent : Key]:
    Key extends `${infer Parent}.${infer Leaf}` ? DotToType<Leaf> : boolean
};

And while it does enforce fields, it stops working if I have multiple paths for the same object.

const foobar: DotToType<"address" | "year" | "owner.address.street"> = {
    "address": true,
    "year": true,
    owner: {
        address: {
            street: true
        }
    }
}; // this works fine


const foobar: DotToType<"address" | "year" | "owner.address.street" | "owner.age"> = {
    "address": true,
    "year": true,
    owner: {
        address: {
            street: true
        }
    }
}; // this only allows the first `owner.{Leaf}`

CodePudding user response:

Your initial code produces a union, which is why the properties are optional:

type DotToType<keys extends string> = {
    [Key in keys]: Key extends `${infer Parent}.${infer Leaf}`
        ? {[key in Parent]: DotToType<Leaf>}
        : {[k in Key]: boolean}
}[keys];

type MyType = DotToType<TFoobar>

// produces:

type MyType = {
    address: boolean;
} | {
    year: boolean;
} | {
    owner: {
        age: boolean;
    };
} | {
    owner: {
        address: {
            street: boolean;
        };
    };
}

You can use the following:

type TFoobar = "address" | "year" | "owner.age" | "owner.address.street";

type BeforeDot<K extends string> = K extends `${infer Parent}.${infer _}` ? Parent : K;
type ChildrenAfterDot<K extends string, P extends string> = K extends `${P}.${infer $Leaf}` ? $Leaf : never;


type DotToType<keys extends string> = {
    [Key in BeforeDot<keys>]: Key extends keys
        ? boolean
        : DotToType<ChildrenAfterDot<keys, Key>>
};

type MyType = DotToType<TFoobar>

// produces: 
type MyType = {
    address: boolean;
    year: boolean;
    owner: DotToType<"age" | "address.street">;
}

Playground link

  • Related