1

I don't understand why, in Typescript, I have this error in a varibale assignement; I think types are compatible, isn't it? To work, I have to add:

async () => {
  dataFiles = <Array<DataFileLoadingInstructionType>>await Promise.all(

but why? The error:

Type '(DataFile | { file_path: string; header_line: number; sheets: never[]; type: string; })[]' is not assignable to type '({ file_path: string; } & { file_info?: string | undefined; file_name_suffix?: string | undefined; command_parameters?: string[] | undefined; } & { type: "unknown"; })[]'.

This is my the index.ts example:

import { DataFileLoadingInstructionType } from "./types_async";
import * as path from "path";

type DataFile = {
  file_path: string;
  type: "unknown";
};

const originalDataFiles: Array<DataFile> = [];
originalDataFiles.push({ file_path: "myFile.txt", type: "unknown" });

let dataFiles: Array<DataFileLoadingInstructionType>;

async function convertPathIfLocal(dataFile: string) {
  if (dataFile.indexOf("://") === -1 && !path.isAbsolute(dataFile)) {
    dataFile = path.join("my_dir/", dataFile);
  }
  return dataFile;
}

(async () => {
//here, I have to add <Array<DataFileLoadingInstructionType>> to work
  dataFiles = await Promise.all(
    originalDataFiles
      .filter((f) => f !== undefined)
      .map(async (dataFile) => {
        if (typeof dataFile === "string") {
          return {
            file_path: await convertPathIfLocal(dataFile),
            header_line: 0,
            sheets: [],
            type: "csv",
          };
        } else {
          dataFile.file_path = await convertPathIfLocal(dataFile.file_path);
          return dataFile;
        }
      })
  );

  console.log(
    `OUTPUT: ${JSON.stringify(
      dataFiles
    )} - type of dataFiles: ${typeof dataFiles}`
  );
})();

And this is my the types.ts example:

import {
  Array,
  Literal,
  Number,
  Partial as RTPartial,
  Record,
  Static,
  String,
  Union,
} from "runtypes";



const UnknownDataFileLoadingInstructionTypeOptions = Record({
  type: Literal("unknown"),
});

export const DataFileLoadingInstructionType = Record({ file_path: String })
  .And(
    RTPartial({
      file_info: String,
      file_name_suffix: String,
      command_parameters: Array(String),
    })
  )
  .And(Union(UnknownDataFileLoadingInstructionTypeOptions));

export type DataFileLoadingInstructionType = Static<
  typeof DataFileLoadingInstructionType
>;

2 Answers 2

1

From what I read of these code snippets, types are actually not assignable.

On one hand, dataFiles is declared with the type Array<DataFileLoadingInstructionType>, in other words:

declare const dataFiles: Array<
  | { file_path: string, file_info?: string, file_name_suffix?: string, command_parameters?: string[] }
  | { type: 'unknown' }
>

On the other hand, the returned value of originalDataFiles.filter(...).map(...) is either:

{
  file_path: string   // await convertPathIfLocal(dataFile)
  header_line: number // 0
  sheets: never[]     // [] inferred as Array<never>
  type: string        // "csv"
}

(cf. the returned object from the if branch, inside the map)

Or:

DataFile

(cf. the returned object from the else branch, inside the map)

So, we end up with:

  • dataFiles type:

    Array<
       | { file_path: string, file_info?: string, file_name_suffix?: string, command_parameters?: string[]}
       | { type: 'unknown' }
    >
    
  • await Promise.all(originalDataFiles.filter(...).map(...)) type:

    Array<
       | { file_path: string, header_line: number, sheets: never[], type: string }
       | DataFile
    >
    

And they are both, indeed, not assignable:

  • The properties header_line, sheets and type are missing on the type DataFileLoadingInstructionType
  • The property file_path is present in DataFile but not in UnknownDataFileLoadingInstructionTypeOptions

I'd say you should:

  • Add the 3 properties to the DataFileLoadingInstructionType, otherwise adapt the returned object in theif branch of the map to make it compatible with DataFileLoadingInstructionType.
  • Remove the property file_path from the dataFile returned in the else branch of the map, or add the file_path property to the UnknownDataFileLoadingInstructionTypeOptions type.
Sign up to request clarification or add additional context in comments.

Comments

1

The essence of the problem lies in misunderstanding string literals, the limitations of type inferencing and duck typing. That is quite a lot, but I will try to explain it bit by bit.

Duck Typing

"If it walks like a duck and it quacks like a duck, then it must be a duck."

One of the nice things in Typescript that you do not need to instantiate a class in order for it to adhere to an interface.

interface Bird {
   featherCount: number
}

class Duck implements Bird {
    featherCount: number;
    constructor(featherInHundreds: number){
        this.featherCount = featherInHundreds * 100;
    }
}

function AddFeathers(d1: Bird, d2: Bird) {
   return d1.featherCount + d2.featherCount;
}

// The objects just need to have the same structure as the
// expected object. There is no need to have 
AddFeathers(new Duck(2), {featherCount: 200});

This creates a lot of flexibility in the language. Flexibility you are using in the map function. You either create an entirely new object where you adjust some things or you adjust the existing dataFile. In this case that might easily be solved by creating a constructor or method that returns a new class. If there are a lot of transformations this might lead to very large classes.

Type Inferencing

However this flexibility comes at a cost in certain cases. Typescript needs to be able to infer the type, but in this case that goes wrong. When you create the new object, the type property is perceived as a string not the template literal "unknown". This exposes two problems in your code.

  1. The type property can only contain a single value "unknown" because it is typed as a string literal with just one value as opposed to a union of multiple literals. The type should at least have the type of "unknown" | "csv" for this value to work. However I expect that this is just a problem in this example since adding the <Array<DataFileLoadingInstructionType>> seems to solve the problem for you, while in this example it would break the example.
  2. But even if you would adjust this or pass the only allowed value "unknown" here it would still complain. This is how Typescript infers types, since you just assign a value here, it presumes that it is the more generic string as opposed to the more narrow literal "csv".

Solution

The trick is helping Typescript type the object you are creating. My suggestion would be to assert the type of the property type, so that Typescript knows that the string assignment is actually a string literal.

Example

import {
    Literal,
    Number,
    Partial as RTPartial,
    Record,
    Static,
    String,
    Union,
} from "runtypes";
import * as path from "path";

// Create separate type, so that we don't need to assert the type inline.
type FileTypes = "unknown" | "csv"

const UnknownDataFileLoadingInstructionTypeOptions = Record({
    type: Literal<FileTypes>("unknown"),
});

export const DataFileLoadingInstructionType = Record({ file_path: String })
    .And(
        RTPartial({
            file_info: String,
            file_name_suffix: String,
            // Threw an error, commented it out for now.
            // command_parameters: Array(String),
        })
    )
    .And(Union(UnknownDataFileLoadingInstructionTypeOptions));

export type DataFileLoadingInstructionType = Static<
    typeof DataFileLoadingInstructionType
>;



type DataFile = {
    file_path: string;
    type: FileTypes;
};

const originalDataFiles: Array<DataFile> = [];
originalDataFiles.push({ file_path: "myFile.txt", type: "unknown" });

let dataFiles: Array<DataFileLoadingInstructionType>;

async function convertPathIfLocal(dataFile: string) {
    if (dataFile.indexOf("://") === -1 && !path.isAbsolute(dataFile)) {
        dataFile = path.join("my_dir/", dataFile);
    }
    return dataFile;
}

(async () => {
    //here, I have to add <Array<DataFileLoadingInstructionType>> to work
    dataFiles = await Promise.all(
        originalDataFiles
            .filter((f) => f !== undefined)
            .map(async (dataFile) => {
                if (typeof dataFile === "string") {
                    return {
                        file_path: await convertPathIfLocal(dataFile),
                        header_line: 0,
                        sheets: [],
                        type: "csv" as FileTypes,
                    };
                } else {
                    dataFile.file_path = await convertPathIfLocal(dataFile.file_path);
                    return dataFile;
                }
            })
    );

    console.log(
        `OUTPUT: ${JSON.stringify(
            dataFiles
        )} - type of dataFiles: ${typeof dataFiles}`
    );
})();

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.