Home > front end >  I want to use Template literal types to represent wildcards
I want to use Template literal types to represent wildcards

Time:05-22

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/rail.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?

CodePudding user response:

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

CodePudding user response:

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