Reputation: 35760
I'm implementing a modular system to process user input, there are transformers to process correspond raw input, and return transformed data.
enum DataType {
Text = 'TEXT',
Image = 'IMAGE',
}
// text transformer module
interface TextInput {
type: DataType.Text,
text: string
}
interface TextOutput {
type: DataType.Text,
text: string
}
const TextTransformer = {
transform(input: TextInput): TextOutput {
return {
type: DataType.Text,
text: input.text,
}
}
}
// image transformer module
interface ImageInput {
type: DataType.Image,
url: string
}
interface ImageOutput {
type: DataType.Image,
url: string
width: number
height: number
}
const ImageTransformer = {
transform(input: ImageInput): ImageOutput {
return {
type: DataType.Image,
url: input.url,
width: 0,
height: 0,
}
}
}
// main module
const transformerMap = {
[DataType.Text]: TextTransformer,
[DataType.Image]: ImageTransformer,
}
type Input = TextInput | ImageInput
type Output = TextOutput | ImageOutput
function transform(inputs: Input[]): Output[] {
return inputs.map(input => {
const transformer = transformerMap[input.type]
return transformer.transform(input)
})
}
Now TypeScript will report error on the transformer.transform(input)
line:
Argument of type 'Input' is not assignable to parameter of type 'TextInput & ImageInput'.
You can play with the example here:
Upvotes: 1
Views: 185
Reputation: 33111
This is classic error.
See this question for more context.
const transformer = transformerMap[input.type]
-> transformer
variable becomes a union of two funcions:
(input: TextInput) => TextOutput
and (input: ImageInput) => ImageOutput
multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.
Hence, you ended up with TextInput & ImageInput
which gives you never
type. This is why you have an error here:
// input is not assignable to never type
return transformer.transform(input)
In order to fix it, you need infer each value which is used in function body, I mean transformerMap
should be passed as a function parameter.
enum DataType {
Text = 'TEXT',
Image = 'IMAGE',
}
interface TextInput {
type: DataType.Text,
text: string
}
interface TextOutput {
type: DataType.Text,
text: string
}
const TextTransformer = {
transform: (input: TextInput): TextOutput => ({
type: DataType.Text,
text: input.text,
})
}
interface ImageInput {
type: DataType.Image,
url: string
}
interface ImageOutput {
type: DataType.Image,
url: string
width: number
height: number
}
const ImageTransformer = {
transform: (input: ImageInput): ImageOutput => ({
type: DataType.Image,
url: input.url,
width: 0,
height: 0,
})
}
const transformerMap = {
[DataType.Text]: TextTransformer,
[DataType.Image]: ImageTransformer,
}
type Input = TextInput | ImageInput
type Output = TextOutput | ImageOutput
type Values<T> = T[keyof T]
const applyTransformer = <
Key extends typeof DataType,
Value extends { transform: (arg: Input) => Output },
TransformMap extends Record<Key & string, Value>>(map: TransformMap) =>
<
Type extends keyof TransformMap,
Inpt extends ({ type: Type }) & Input,
Inputs extends Inpt[]
>(inputs: [...Inputs]): Output[] =>
inputs.map(input => map[input.type].transform(input))
const withTransformer = applyTransformer({
[DataType.Text]: TextTransformer,
[DataType.Image]: ImageTransformer,
})
const transformText = withTransformer([{
type: DataType.Text,
text: 'string'
}]) // ok
const transformImage = withTransformer([{
type: DataType.Image,
url: 'string'
}]) // ok
const _ = withTransformer([{
type: DataType.Text,
}]) // expected error
const __ = withTransformer([{
type: DataType.Image,
}]) // expected error
You can find more information in my blog here and here
If you are interested in function arguments infering see this article.
Upvotes: 1