2

Is it possible to construct some type directly from the string provided?

I want to create a type something like shown below:

type MyConfiguration<T> = {
  items: T[];
  onChange: (updated: ConstructedType<T>) => void;
}

const conf: MyConfiguration = {
  items: ['id', 'nested.id', 'nested.name'],
  onChange: updated => {
    console.log(`You updated ${updated.nested.name}(id: ${updated.nested.id})`);
  },
};

So it will generate a type for updated to be {id: string; nested: { id: string; name: string}}

Jeggy
  • 1,474
  • 1
  • 19
  • 35
  • 1
    so `id` and `nested.id` etc. should be properties of `updated` of type `string`? – Tobias S. May 03 '22 at 13:05
  • the `updated` should have generated a type of `{id: string; nested: { id: string; name: string}}` – Jeggy May 03 '22 at 13:09
  • 1
    You might want to edit the question to include that. Someone voted to close because of the lack of details. – kelsny May 03 '22 at 13:47

1 Answers1

2

This solution might not be perfect but the type of updated seems to be correct:

type First<T extends string> = T extends `${infer L}.${string}` ? L : T
type Nested<T extends string> = T extends `${string}.${infer R}` ? R : string

type _ConstructedType<T extends string> = string extends Nested<T> ? string : {
  [Key in T as First<Nested<T>>]: _ConstructedType<Nested<T>>
}

type ConstructedType<K extends readonly string[]> = {
  [Key in K[number] as First<Key>]: _ConstructedType<Key>
}

function createConf<K extends readonly string[]>(conf: {items: K, onChange: (updated: ConstructedType<K>) => any}) {
    return conf
}

createConf({
  items: ['id', 'nested.id', 'nested.name'] as const,
  onChange: updated => {
    console.log(`You updated ${updated.nested.name}(id: ${updated.nested.id})`);
  },
})

In your question you specified that you want to have a type called MyConfiguration. A type alone cannot enforce any constraints between properties. So we have to create a factory function called createConf. We can now pass a conf object to createConf and all types are inferred.

A drawback that I don't know how to fix yet, is that you have to write as const behind the items array. Otherwise TypeScript will infer the type as string[] and not as a tuple.

Playground


Thanks to @jcalz for this version which fixes the issues:

type ConstructedType<K extends string> = {
  [P in K as P extends `${infer L}.${string}` ? L : P]:
  [P] extends [`${string}.${infer R}`] ? ConstructedType<R> : string;
}

function createConf<K extends string>(conf:
  { items: readonly K[], onChange: (updated: ConstructedType<K>) => any }) {
  return conf
}

type Test = ConstructedType<'id' | 'nested.id' | 'nested.child.name'>

const x = createConf({
  items: ['id', 'nested.id', 'nested.name', 'nested.child.name'],
  onChange: updated => {
    console.log(`You updated ${updated.nested.name}(id: ${updated.nested.id})`);
  },
})

He also provided an alternative solution.

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • Error in playground! Type `readonly string[]` is not assignable to type `string[]`. Change all your types to use `extends readonly string[]`. – kelsny May 03 '22 at 14:11
  • @catgirlkelly oops, edited – Tobias S. May 03 '22 at 14:12
  • 1
    To get around the `as const` too, you could use variadic arguments like [this](https://tsplay.dev/N9EoMN). It's not the cleanest however. – kelsny May 03 '22 at 14:17
  • This is great, but would it be possible to make it recursive? so `nested.child.name` would also work. – Jeggy May 03 '22 at 14:30
  • @Jeggy it is recursive but I must have made a mistake. Let me try to fix it – Tobias S. May 03 '22 at 14:35
  • @Jeggy I made another mistake, its still not working correctly – Tobias S. May 03 '22 at 14:41
  • 2
    You might want to change it to [this version](https://tsplay.dev/w8o9pW) instead? For what it's worth, [this version](https://tsplay.dev/Nlvj5W) is probably how I'd have approached it although I do think key remapping is neater. – jcalz May 03 '22 at 17:59
  • @jcalz nice solution. But I am confused. What is the use of the function where you put `CT1` in the parameters and then `infer I` later? Is there any documentation on this? – Tobias S. May 03 '22 at 18:15
  • 1
    I see a familiar `Exp` type in there... Anyways I think it's got something to do with naked types? Ah wait this looks like union to intersection! – kelsny May 03 '22 at 18:18
  • 1
    `CT1` only works for a single dotted key. Then what I do is break `K` apart into its union constituents and compute the *intersection* of `CT1` for all the `K` elements. That is enough, but the type is ugly `Record<"a", Record<"b", string>> & `Record<"a", Record<"c", Record<"d", string>>>` so the `Exp` is `ExpandRecursively` from [this q/a](https://stackoverflow.com/q/57683303/2887218) to give `{a: {b: string, c: {d: string}}}`. – jcalz May 03 '22 at 18:32