打字稿中的类型分配

Type assignment in Typescript

我不明白为什么在 Typescript 中,我在可变赋值中有这个错误;我认为类型是兼容的,不是吗? 要工作,我必须添加:

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

但是为什么呢? 错误:

类型 '(DataFile | { file_path: string; header_line: number; sheets: never[]; type: string; })[]' 不能分配给类型 '({ file_path: 字符串; } & { file_info?: 字符串 | 未定义; file_name_suffix?: 字符串 | 未定义; command_parameters?: 字符串[] | 未定义; } & { 类型:“未知”;})[]'。

这是我的 index.ts 示例:

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}`
  );
})();

这是我的 types.ts 示例:

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
>;

根据我读到的这些代码片段,类型实际上是不可分配的。

一方面,dataFiles 声明为 Array<DataFileLoadingInstructionType> 类型,换句话说:

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

另一方面,originalDataFiles.filter(...).map(...) 的返回值是:

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

(参见 if 分支返回的对象,在 map 内)

或者:

DataFile

(参见 else 分支返回的对象,在 map 内)

所以,我们最终得到:

  • dataFiles 类型:

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

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

事实上,它们都是不可分配的:

  • 类型 DataFileLoadingInstructionType
  • 缺少属性 header_linesheetstype
  • 属性 file_path 存在于 DataFile 但不存在于 UnknownDataFileLoadingInstructionTypeOptions

我会说你应该:

  • 将 3 个属性添加到 DataFileLoadingInstructionType,否则在 mapif 分支中调整返回的对象以使其与 DataFileLoadingInstructionType 兼容。
  • mapelse 分支返回的 dataFile 中删除 属性 file_path,或添加 file_path 属性 到 UnknownDataFileLoadingInstructionTypeOptions 类型。

问题的本质在于误解了字符串文字,类型推断和鸭子类型的局限性。那是相当多,但我会尝试一点一点地解释它。

鸭子打字

“如果它走路像鸭子,叫起来像鸭子,那它一定是鸭子。”

Typescript 的优点之一是您无需实例化 class 即可使其遵循接口。

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});

这为语言创造了很大的灵活性。您在 map 函数中使用的灵活性。您要么创建一个全新的对象,在其中调整一些内容,要么调整现有的 dataFile。在这种情况下,可以通过创建 returns 一个新的 class 的构造函数或方法轻松解决。如果有很多转换,这可能会导致非常大的 classes.

类型推断

然而,在某些情况下,这种灵活性是有代价的。 Typescript 需要能够推断类型,但在这种情况下出错了。当您创建新对象时,type 属性 被视为 string 而不是模板文字 "unknown"。这暴露了您的代码中的两个问题。

  1. type 属性 只能包含一个值 "unknown" 因为它被类型化为只有一个值的字符串文字,而不是多个文字的并集。该类型至少应具有 "unknown" | "csv" 类型才能使该值起作用。但是我希望这只是这个例子中的一个问题,因为添加 <Array<DataFileLoadingInstructionType>> 似乎可以为你解决问题,而在这个例子中它会破坏这个例子。
  2. 但即使您调整它或在此处传递唯一允许的值 "unknown",它仍然会抱怨。这就是 Typescript 推断类型的方式,因为您只是在这里分配一个值,它假定它是更通用的字符串,而不是更窄的文字 "csv".

解决方案

诀窍是帮助 Typescript 键入您正在创建的对象。我的建议是断言 属性 type 的类型,以便 Typescript 知道字符串赋值实际上是字符串文字。

例子

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}`
    );
})();