wong2
wong2

Reputation: 35760

Implementing a modular system with TypeScript

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:

Playground

Upvotes: 1

Views: 185

Answers (1)

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

Playground

You can find more information in my blog here and here

If you are interested in function arguments infering see this article.

Upvotes: 1

Related Questions