DEV Community

I need to learn about TypeScript Template Literal Types

Craig ☠️💀👻 on September 27, 2020

It's Sunday in New Zealand and I don't want to get out of bed yet, so instead I'm going to listen to the new Menzingers album and learn about TypeS...
Collapse
 
nandorojo profile image
Fernando Rojo

Seriously amazing article. I wish there was more legible content like this about advanced Typescript types. I thought your approach of using the typefunction made it especially easy to understand. Have you considered opening an RFC on the typefunction syntax? It’s a really interesting idea.

I would love to read more content like this, and encourage you to keep contributing.

Collapse
 
nroboto profile image
Mike Nightingale

Thanks for the great article.

I was messing around with the string dot Path type, I think it can be made a little simpler by splitting it into two types

type Subpath<T, Key extends keyof T = keyof T> = (
  T[Key] extends Record<string, any> ?
    `${Key & string}.${Path<T[Key], Exclude<keyof T[Key], keyof any[]>>}`
  :
    never
);

type Path<T, Key extends keyof T = keyof T> = (
  Key extends string ?
    Key | Subpath<T, Key>
  :
    never
);
Enter fullscreen mode Exit fullscreen mode

Subpath gets the paths for properties of T, and Path stitches them all together into a single type.

PathType can also be made a lot more concise by using type intersection on Key and Rest

type PathValue<T, P extends Path<T>> = (
  P extends `${infer Key}.${infer Rest}` ?
    PathValue<T[Key & keyof T], Rest & Path<T[Key & keyof T]>>
  :
    T[P & keyof T]
);
Enter fullscreen mode Exit fullscreen mode

I'm not sure if this is easier to understand than the original though.

Here's the result.

Collapse
 
seanblonien profile image
Sean Blonien • Edited

This is great stuff!

I have been using your improved types for a better part of this last year, and I came into an example of a type that breaks the interface, and I honestly have no idea why. (Also doesn't work with original type)

Counter example of the interface not correctly idenying foo.test

@nroboto do you know what's going on here? Am i missing something?

Collapse
 
nroboto profile image
Mike Nightingale

The issue is that in the Subpath type we exclude all of the properties of an array, which includes the length property, this results in foo.length being excluded even though in this case length is a property on an object. One way to fix this is to change the exclusion to only apply if the type is an array:

type ExcludeArrayKeys<T> = Exclude<keyof T, T extends any[] | readonly any[] ? keyof any[] : never>

type Subpath<T, Key extends keyof T = keyof T> = (
  T[Key] extends Record<string, any> ?
    `${Key & string}.${Path<T[Key], ExcludeArrayKeys<T[Key]>>}`
  :
    never);
Enter fullscreen mode Exit fullscreen mode

Here's the TS playground.

Collapse
 
nielsboecker profile image
Niels Boecker

Hey Craig, these new TypeScript functionalities are blowing my mind and your article was amazing, really appreciated how you broke down the fairly hard to digest type definitions into smaller steps to help follow the thought process. :)

I have a nitpick, there seems to be a difference in your rewrite of the type-safe string dot notation example. Unlike the original code example, it will not accept top-level properties:

const user = {
  age: 12,
  projects: [
    { name: "Cool project!", contributors: 10 },
    { name: "Amazing project!", contributors: 12 },
  ]
} as const;

// This is not working :(
get(user, 'age');
Enter fullscreen mode Exit fullscreen mode
Collapse
 
phenomnominal profile image
Craig ☠️💀👻

Nice catch! I'm not surprised that I broke something! Hopefully people don't use my version for real, as it was strictly for me trying to work it all out 😅

Collapse
 
phillyx profile image
Phillyx

Hey Craig, when I use Interface, it is not working on Array.Number

// This is not working
setData(person, 'cars.1.brand', 'BYD-YUAN')
Enter fullscreen mode Exit fullscreen mode

here is the code

Looking forward to Ur help

Collapse
 
phillyx profile image
Phillyx • Edited

Thx to @nroboto . Finally I've solved type-safe array dot notation.

here is the code-intelligence

See here

Collapse
 
phillyx profile image
Phillyx

Update for derivation of array types
type SubArrayPath

= P extends PropertyKey ? ${number} : ${number} | ${number}.${PathTemp<P>}

Collapse
 
malcolmkee profile image
Malcolm Kee

One question about PathValue. With your implementation above, we get never with get(user, 'projects.0.name').

But it will get the correct type with one small change:

type Path<T, Key extends keyof T = keyof T> =
  (Key extends string
  ? T[Key] extends Record<string, any>
    ? | `${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<any>>> & string}`
      | `${Key}.${Exclude<keyof T[Key], keyof Array<any>> & string}`
+      | Key
-       | never
    : never
  : never)
Enter fullscreen mode Exit fullscreen mode

See here.

Collapse
 
sirseanofloxley profile image
Sean Allin Newell

Wowza. Gonna have TS written in TS Types soon.

Collapse
 
roszykdamian profile image
Damian Roszyk

Thanks for this great article!

Collapse
 
captainyossarian profile image
yossarian

Awesome article!

with great power comes great responsibility :)))

Collapse
 
llgcode profile image
llgcode • Edited

Thanks for this article, I learn a lot with your explanation. good album too :-)
I'll keep it in my favorite as a manual to understand template literal type