My basic TypeScript is this:

await convert({
    input: { file: { path: "./my.docx" } },
    output: { file: { path: "./my.pdf" } },
});

await convert({
    input: { value: { r: 1, g: 10, b: 100 }, format: 'rgb' },
    output: { format: 'hex' },
});

export type FileIO = {
    input: {
        file: {
            path: string // should be typed based on extension.
        }
    }
    output: {
        file: {
            path: string // should be typed based on extension.
        }
    }
}

export type FormatIO = {
    input: {
        format: 'rgb' | 'hex'
        value: any
    }
    output: {
        format: 'rgb' | 'hex' // in reality, can't be the same as the input format.
    }
}

function convert(source: FileIO | FormatIO) {
    if ('file' in source.input) {
        console.log('file io', source)
    } else {
        console.log('format io', source)
    }
}

I would like to add type annotations that scope the “extra” arguments (outside input/output props) to either the file extension or the format specified by both input and output. Is that possible? So ideally you could do this:

await convert({
    input: { file: { path: "./my.docx" } },
    // say 'margin' only exists on pdf and html outputs
    output: { file: { path: "./my.pdf" }, margin: 10, },
    // say the 'tool' you can select is based on the input/output file extension combo.
    cliTool: 'pandoc'
});
await convert({
    input: { file: { path: "./my.docx" } },
    // SUCCESS
    output: { file: { path: "./my.html" }, margin: 10, },
});
await convert({
    input: { file: { path: "./my.docx" } },
    // ERROR
    output: { file: { path: "./my.ppt" }, margin: 10, },
});
await convert({
    input: { file: { path: "./my.png" } },
    output: { file: { path: "./my.jpg" } },
    cliTool: 'pandoc' // error, these extensions don't belong to pandoc tool.
});

await convert({
    input: { value: { r: 1, g: 10, b: 100 }, format: 'rgb' },
    output: { format: 'hex' },
});

export type FileIO = {
    input: {
        file: {
            path: string // should be typed based on extension.
        }
    }
    output: {
        file: {
            path: string // should be typed based on extension.
        }
    }
    cliTool?: 'ebook-convert' | 'soffice' | 'pandoc'
}

export type FormatIO = {
    input: {
        format: 'rgb' | 'hex'
        value: any
    }
    output: {
        format: 'rgb' | 'hex' // in reality, can't be the same as the input format.
    }
}

function convert(source: FileIO | FormatIO) {
    if ('file' in source.input) {
        console.log('file io', source)
    } else {
        console.log('format io', source)
    }
}

I saw this Color hex type in TypeScript:

type HexaNumber = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type HexaLetter = 'A' | 'B' | 'C' | 'D' | 'E' | 'F';
type HexaChar = HexaLetter | HexaNumber;
type Hexa3 = `${HexaChar}${HexaChar}${HexaChar}`;
type Hexa6 = `${Hexa3}${Hexa3}`;
type Hexa8 = `${Hexa6}${HexaChar}${HexaChar}`;
type ColorValueHexComplex = `#${Hexa3 | Hexa6 | Hexa8}`;

But as they stated:

We cover all the use cases, but we faced another issue. We found that typescript put in memory all possible the possible values to evaluate a type. It meant to us 16⁸=4,294,967,296 namely more than 4 billions ! So we had the TS2590: Expression produces a union type that is too complex to represent error from our 16Gb RAM macs.

There are about 400×400 combinations of inputs/outputs I’m dealing with, so that’s about 160,000 types, probably too many for TypeScript to do something similar? Is there a way to make it a little more dynamic? That is, there are really only 400 file extensions to deal with, so maybe we can have a FileIO type which reuses that and doesn’t generate a combinatorial explosion of types?

I got the first part figured out I think:

await conv({
    input: { file: { path: "./my.docx" } },
    // say 'margin' only exists on pdf and html outputs
    output: { file: { path: "./my.pdf" }, margin: 10, },
    // say the 'tool' you can select is based on the input/output file extension combo.
    cliTool: 'ebook-convert'
});

type InputFileType = 'html' | 'png' | 'docx'
type OutputFileType = 'pdf' | 'jpg'

function conv<I extends InputFileType, O extends OutputFileType>(props: FileIO<I, O>) {

}

export type FileIO<I extends InputFileType, O extends OutputFileType> = {
    input: {
        file: {
            path: `${string}.${I}`
        }
    }
    output: {
        file: {
            path: `${string}.${O}` // should be typed based on extension.
        }
    }
} & (O extends 'pdf' ? {
    cliTool: 'ebook-convert'
    output: { margin: number }
} : {
    cliTool: 'pandoc'
})

But now how do you make it a little more generic / robust? By that I mean, it should have custom props based on specific input/output combinations in some cases, and sets of inputs/outputs in other cases. Basically, where I do O extends 'pdf' ? ..., my question boils down to how can I make that dynamic, so I don’t have to do basically this:

O extends 'pdf'
? thisSetOfProps
: O extends 'ext2'
? props2
: O extends 'ex3'
? props3
...

It would also have to take into account I, making that nested ternaries super deep. How can I transform this into a map of some sort? The goal would be, once you specify the input/output file paths (focusing on FileIO for now), you get typescript autocomplete on the rest of the properties.

I think I basically need to do something like this:

export type IO = {
  html: {
    pdf: props1,
    docx: props2,
  }
  pdf: {
    html: props3
  }
}

But for every combination of props.

export type IO = {}

INPUT_FORMAT.forEach(i => {
  OUTPUT_FORMAT.forEach(o => {
    IO[i][o] = propsn
  })
})

Obviously that’s out of the question. But is there a TypeScript way to somewhat get at what I’m describing? Or should I resort to code generation perhaps?