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