0

I am trying to build a required object parameter depending on which path you target in a object

type Split<S extends string, D extends string> =
    string extends S ? string[] :
    S extends '' ? [] :
    S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
    [S];

type PropType<T, Path extends string> =
    string extends Path ? unknown :
    Path extends keyof T ? T[Path] :
    Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
    unknown; 

type ParseMustaches_<T extends string> = 
  T extends `${infer U}{{${infer V}}}${infer W}` 
    ? Record<V, string> 
    : never

type ParseMustaches<T extends string[]> = ParseMustaches_<T[number]>;

type Prop<T, K extends keyof T> = T[K];

declare function translate<T extends { [L in K]: string }, K extends string>(obj: T, path: K, placeholders: ParseMustaches<Split<T[K], " ">>): void;

const obj = {
  "title": "Welcome to {{sitename}}, {{user}}",
  "button": {
    "text": "Click here to go to {{location}}",
    "num": 5
  }
} as const;

translate(obj, "title", { sitename: "", user: "" }) // works
translate(obj, "button.text", { }) // does not work

It seems to work for top level properties but nested properties fail, how could I fix this? link to playground

1 Answer 1

1

The problem lies in the part where you said T extends { [L in K]: string }, since K here is not a key in general, but a path. So you need a little extra work to make your magic happen.

type Split<S extends string, D extends string> =
  string extends S ? string[] :
  S extends '' ? [] :
  S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
  [S];

type PropType<T, Path extends string> =
  string extends Path ? unknown :
  Path extends keyof T ? T[Path] :
  Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
  unknown;

type ParseMustaches_<T extends string> =
  T extends `${infer U}{{${infer V}}}${infer W}`
  ? Record<V, string>
  : never

type ParseMustaches<T extends string[]> = ParseMustaches_<T[number]>;

// You need to this utility type
type TypeOnPath<Path extends string, T> =
  string extends Path ? unknown :
  Path extends `${infer K}.${infer R}` ? { [k in K]: TypeOnPath<R, T> } :
  { [k in Path]: T };

declare function translate<K extends string, T extends TypeOnPath<K, string>>
  (obj: T, path: K, placeholders: ParseMustaches<Split<PropType<T, K> & string, " ">>): void;

const obj = {
  "title": "Welcome to {{sitename}}, {{user}}",
  "button": {
    "text": "Click here to go to {{location}}",
    "num": 5
  }
} as const;

translate(obj, "title", { sitename: "", user: "" })
translate(obj, "button.text", {}) // now we have an error saying that 'location' is missing
translate(obj, "button.num", {}) // correctly report error saying 'button.num' is not a string

See this Playground Link

Update

To get the desired behavior mentioned in the comment, you can modify Split:

// Remove constraint on S so that it takes any input type
type Split<S, D extends string> =
  S extends string ? (
    string extends S ? string[] :
    S extends '' ? [] :
    S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
    [S]
  ) : never;

type PropType<T, Path extends string> =
  string extends Path ? unknown :
  Path extends keyof T ? T[Path] :
  Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
  unknown;

type ParseMustaches_<T extends string> =
  T extends `${infer U}{{${infer V}}}${infer W}`
  ? Record<V, string>
  : never

type ParseMustaches<T extends string[]> = ParseMustaches_<T[number]>;

type TypeOnPath<Path extends string, T> =
  string extends Path ? unknown :
  Path extends `${infer K}.${infer R}` ? { [k in K]: TypeOnPath<R, T> } :
  { [k in Path]: T };

// This time we use TypeOnPath<K, any>
declare function translate<K extends string, T extends TypeOnPath<K, any>>
  (obj: T, path: K, placeholders: ParseMustaches<Split<PropType<T, K>, " ">>): void;

const obj = {
  "title": "Welcome to {{sitename}}, {{user}}",
  "button": {
    "text": "Click here to go to {{location}}",
    "num": 5
  }
} as const;

translate(obj, "title", { sitename: "", user: "" })
translate(obj, "button.text", {}) // now we have an error saying that 'location' is missing
translate(obj, "button.num", {}) // error with never

See this Playground Link

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

3 Comments

Thank you! but that last error is not wanted behaviour, the path can resolve to any value in the object, it's fine if it's a number or string or array
But how are you supposed to parse the value then? Your ParseMustaches_ only accepts string.
If the value resolves to something that's not a string, the 3rd param should be never, just like when there are no mustache notations

Your Answer

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