Construct type information from string via template literal

Viewed 87

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}}

1 Answers

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.

Related