I want to use Template literal types to represent wildcards

Viewed 84

I am working on typing existing javascript files in typescript for my business.

Here is a sample object:

[
  {
    // The column names givenName, familyName, and picture are ones of examples.
    "givenName": {
      "text": "Foo",
      "type": "text"
    },
    "familyName": {
      "text": "Bar",
      "type": "text"
    },
    "picture": {
      "text": "abc.png",
      "type": "image",
      "thumbnail": "https://example.com/thumbnail/sample.png"
    },
    // The following two properties are paths to the PDF and thumbnail generated from the above information.
    "pdf62882329b9baf800217efe7c": "https://example.com/pdf/genarated_pdf.pdf",
    "thumbnail62882329b9baf800217efe7c": [
      "https://example.com/thumbnail/head.png",
      "https://example.com/thumbnail/tail.png"
    ]
  },
  {
    // ... (The structure is the same as above object.)
  }, // ...
]

I would like to type the object part as follows:

type Row = {
  [headerKey: string]: {
    text: string;
    type: "text";
  } | {
    text: string;
    type: "image";
    thumbnail: string;
  };
  // The following two properties are the paths to the generated PDF and thumbnails.
  // The reason for concatenating the id is to avoid name conflicts when the column names "pdf" and "thumbnail" come in the column name. 
  // (Since the string for id is randomly generated, I believe it is unlikely that this string will be used for the column name.)
  pdf+id: string; // path to the generated PDF
  thumbnail+id: [string, string]; // path to the generated thumbnail (It have two elements because the image has two sides.)
};

And I used Template literal types, typed as follows:

type Row = {
  [headerKey: string]: {
    text: string;
    type: "text";
  } | {
    text: string;
    type: "image";
    thumbnail: string;
  };
  [pdfKey: `pdf${string}`]: string;
  [thumbnailKey: `thumbnail${string}`]: [string, string];
};

But it doesn't work as expected. Is there any way to successfully type this object?

2 Answers

I also think that it is not possible to put this logic inside a single type in TypeScript. But it is still possible to validate such a structure when using a generic function.

When we pass the an object to a generic function, we can use a generic type to validate the type of the object.

function createRow<T extends ValidateRow<T>>(row: T): T {
  return row
}

Now all we need is the generic type.

type ValidateRow<T> = {
  [K in keyof T]: K extends `pdf${string}`
    ? string
    : K extends `thumbnail${string}` 
      ? readonly [string, string]
      : {
          readonly text: string;
          readonly type: "text";
        } | {
          text: string;
          type: "image";
          thumbnail: string;
        }    
}

As you can see, this type follows a simple if/else logic to determine the correct type for each property name.

Now let's test it with a valid object:

createRow({    
  "givenName": {
    "text": "Foo",
    "type": "text"
  },
  "familyName": {
    "text": "Bar",
    "type": "text"
  },
  "picture": {
    "text": "abc.png",
    "type": "image",
    "thumbnail": "https://example.com/thumbnail/sample.png"
  },   
  "pdf62882329b9baf800217efe7c": "https://example.com/pdf/genarated_pdf.pdf",
  "thumbnail62882329b9baf800217efe7c": [
    "https://example.com/thumbnail/head.png",
    "https://example.com/thumbnail/rail.png"
  ]
})
// No error!

This passes the check. Let's try some errors:

createRow({    
  "givenName": {
    "text": "Foo",
    "type": "text"
  },
  "familyName": {
    "text": "Bar",
    "type": "text"
  },
  "picture": {
    "text": "abc.png",
    "type": "image",
    "thumbnail": "https://example.com/thumbnail/sample.png"
  },   
  "pdf62882329b9baf800217efe7c": "https://example.com/pdf/genarated_pdf.pdf",
  "thumbnail62882329b9baf800217efe7c": [
    "https://example.com/thumbnail/head.png"
  ]
})
// Error: Type '[string]' is not assignable to type 'readonly [string, string]'


createRow({    
  "givenName": {
    "text": "Foo",
    "type": "text"
  },
  "familyName": {
    "text": "Bar",
    "type": "text2"
  },
  "picture": {
    "text": "abc.png",
    "type": "image",
    "thumbnail": "https://example.com/thumbnail/sample.png"
  },   
  "pdf62882329b9baf800217efe7c": "https://example.com/pdf/genarated_pdf.pdf",
  "thumbnail62882329b9baf800217efe7c": [
    "https://example.com/thumbnail/head.png",
    "https://example.com/thumbnail/rail.png"
  ]
})
// Error: Type '"text2"' is not assignable to type '"text" | "image"'. Did you mean '"text"'

So we can validate objects even with complex logic as long as we use some generic function.

Playground

If you don't mind declaring all the keys for your object early on, you can define your Row type as:

type Name = {
    text: string;
    type: "text";
};
type RowShape = {
  thumbnail: [string, string];
  pdf: string;
  givenName: Name;
  familyName: Name;
  picture:  {
    text: string;
    type: "image";
    thumbnail: string;
  };
}

type Row = {
  [x in keyof RowShape 
    as `${x extends 'pdf' ? 
        `pdf${string}`: x extends 'thumbnail'? 
          `thumbnail${string}`: x}`
  ]: RowShape[x]
}
Related