简体   繁体   中英

Typescript: How to use discriminated unions with structure rather than with discriminant property?

I want to write a function to retrive the file content from source file. The source file has only two kinds of structure. I want to use discriminated unions with user-defined type guard to handle the content.

I need exhaustiveness checking so I turned on the strictNullChecks . I expect that if it creates discriminated unions succesfully, the compiler will not add | undefined | undefined to the returned fileContent in function retrieveFileContent . But the Typescript compiler throw an error:

error TS2322: Type '{ type: FileContentType; payload: string; } | { type: FileContentType; payload: { filename: string; path: string; }; } | undefined' is not assignable to type 'FileContent'.
Type 'undefined' is not assignable to type 'FileContent'

How can I modify my code to do discriminated unions with user-defined type guard right? Or is there any other better solution for my use case?

Sorce Code:

// Target File Content Format

enum FileContentType {
  disk,
  memory,
}

interface FileContentInMemory {
  type: FileContentType.memory
  payload: string
}

interface FileContentInDisk {
  type: FileContentType.disk
  payload: {
    filename: string
    path: string
  }
}

type FileContent = FileContentInMemory | FileContentInDisk

// Source File Containing Content

interface FileBasics {
  fieldname: string
  originalname: string
  encoding: string
  mimetype: string
  size: number
}

interface FileInMemory extends FileBasics {
  fileString: string
}

interface FileInDisk extends FileBasics {
  filePath: string
}

type SourceFile = FileInMemory | FileInDisk

function isInMemory(file: SourceFile): file is FileInMemory {
  return (<FileInMemory>file).fileString !== undefined
}

function isInDisk(file: SourceFile): file is FileInDisk {
  return (<FileInDisk>file).filePath !== undefined
}

// Retrieve Content From File

function retrieveFileContent(file: SourceFile): FileContent {
  let fileContent
  if (isInMemory(file)) {
    fileContent = {
      type: FileContentType.memory,
      payload: file.fileString
    }
  } else if (isInDisk(file)) {
    fileContent = {
      type: FileContentType.disk,
      payload: {
        filename: file.originalname,
        path: file.filePath,
      }
    }
  }
  return fileContent
}

My tsconfig.json :

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "esModuleInterop": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "outDir": "dist",
    "sourceMap": true,
    "strictNullChecks": true,
    "rootDir": "src",
    "jsx": "react",
    "types": [
      "node",
      "jest"
    ],
    "typeRoots": [
      "node_modules/@types"
    ],
    "target": "es2016",
    "lib": [
      "dom",
      "es2016"
    ]
  },
  "include": [
    "src/**/*"
  ]
}

As far as I know, that type of exhaustiveness checking only works with a switch statement that is the last statement in a function :

type DiscriminatedUnion = {k: "Zero", a: string} | {k: "One", b: string};

// works
function switchVersion(d: DiscriminatedUnion): string {
  switch (d.k) {
    case "Zero": return d.a;
    case "One": return d.b;
  }
}

// broken
function ifVersion(d: DiscriminatedUnion): string {
// Error! might not return a string -----> ~~~~~~  
  if (d.k === "Zero") return d.a;
  if (d.k === "One") return d.b;  
}

And the check you're doing is not really suited to a switch statement. Luckily, you could use the second method of exhaustiveness checking listed in the docs : using a helper function which throws a compiler error if file is not narrowed to never after all the checks have completed:

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

function retrieveFileContent(file: SourceFile): FileContent {
  let fileContent: FileContent; // use annotation to avoid any
  if (isInMemory(file)) {
    fileContent = {
      type: FileContentType.memory,
      payload: file.fileString
    }
  } else if (isInDisk(file)) {
    fileContent = {
      type: FileContentType.disk,
      payload: {
        filename: file.originalname,
        path: file.filePath,
      }
    }
  } else return assertNever(file); // no error, file is never      
  return fileContent; // no error, fileContent is FileContent
}

Hope that helps. Good luck!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM