我想使用模板文字类型来表示通配符

I want to use Template literal types to represent wildcards

我正在为我的业务在打字稿中输入现有的 javascript 文件。

这是一个示例对象:

[
  {
    // 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.)
  }, // ...
]

我想输入对象部分如下:

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.)
};

并且我使用了模板字面量类型,类型如下:

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

但它没有按预期工作。 有什么方法可以成功键入此对象?

我也认为不可能将此逻辑放在 TypeScript 中的单个类型中。但是在使用泛型函数时仍然可以验证这样的结构。

当我们将对象传递给泛型函数时,我们可以使用泛型类型来验证对象的类型。

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

现在我们只需要泛型。

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

如您所见,此类型遵循简单的 if/else 逻辑来确定每个 属性 名称的正确类型。

现在让我们用一个有效的对象来测试它:

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!

这通过了检查。让我们尝试一些错误:

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"'

因此,只要我们使用一些通用函数,即使逻辑复杂,我们也可以验证对象。

Playground

如果您不介意尽早声明对象的所有键,您可以将 Row 类型定义为:

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