From 291e4eee53681bc77ae75f7484d78fafee90e3af Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 27 Nov 2023 17:44:27 +0800 Subject: [PATCH] Improve types for read[File], write[File], sheet_to_json --- types/index.d.ts | 67 ++++++++++++++++------ types/xlsx-tests.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 25b8d24..a9cf0a8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -19,21 +19,50 @@ export function set_fs(fs: any): void; /** Set internal codepage tables */ export function set_cptable(cptable: any): void; -/** NODE ONLY! Attempts to read filename and parse */ -export function readFile(filename: string, opts?: ParsingOptions): WorkBook; -/** Attempts to parse data */ -export function read(data: any, opts?: ParsingOptions): WorkBook; +/** + * Attempts to synchronously read and parse a file from its supplied file path. + * + * NOTE: This function is only supported in server-side environments that + * provide synchronous file-reading APIs (e.g. NodeJS) + */ +export function readFile(filePath: string, opts?: ParsingOptions): WorkBook; +/** + * Attempts to parse the data supplied + * + * @param data the data to parse, in one of the following formats: + * + * - `"buffer"` (`Uint8Array` or NodeJS `Buffer`): binary data consisting of + * 8-bit unsigned integers + * - `"array"` (`ArrayBuffer`) + * - `"base64"` (`string`): Base64 encoding of the file + * - `"file"` (`string`): path of file that will be read (server-side + * environments only. See also {@link readFile}.) + * - `"string"` (`string`): JS string (only appropriate for UTF-8 text formats) + * - `"binary"` (`string`): binary string (byte `n` is `data.charCodeAt(n)`) + * + * Some common formats are automatically deduced from the data input type, + * including `Uint8Array` and `ArrayBuffer` objects and NodeJS `Buffer` + * objects. Alternatively, the format can be explicitly supplied as + * `opts.type`. + * + * When a string is passed with no `type`, the library assumes the data is a + * Base64 string. `FileReader#readAsBinaryString` or ASCII data requires + * "binary" type. DOM strings, including `FileReader#readAsText`, should use + * type "string". + */ +export function read(data: ArrayBuffer | Uint8Array | string, opts?: ParsingOptions): WorkBook; /** Attempts to write or download workbook data to file */ -export function writeFile(data: WorkBook, filename: string, opts?: WritingOptions): any; +export function writeFile(data: WorkBook, filename: string, opts?: WritingOptions): void; /** Attempts to write or download workbook data to XLSX file */ -export function writeFileXLSX(data: WorkBook, filename: string, opts?: WritingOptions): any; +export function writeFileXLSX(data: WorkBook, filename: string, opts?: WritingOptions): void; /** Attempts to write or download workbook data to file asynchronously */ type CBFunc = () => void; -export function writeFileAsync(filename: string, data: WorkBook, opts: WritingOptions | CBFunc, cb?: CBFunc): any; +export function writeFileAsync(filename: string, data: WorkBook, opts: WritingOptions | CBFunc, cb?: CBFunc): void; /** Attempts to write the workbook data */ -export function write(data: WorkBook, opts: WritingOptions): any; +export function write(data: WorkBook, opts: WritingOptions): + T extends 'buffer' ? Uint8Array : T extends 'array' ? ArrayBuffer : string; /** Attempts to write the workbook data as XLSX */ -export function writeXLSX(data: WorkBook, opts: WritingOptions): any; +export function writeXLSX(data: WorkBook, opts: WritingOptions): void; /** Utility Functions */ export const utils: XLSX$Utils; @@ -253,11 +282,12 @@ export interface ParsingOptions extends UTCOption, CommonOptions, DenseOption { PRN?: boolean; } +type WritingOptionsType = 'base64' | 'binary' | 'buffer' | 'file' | 'array' | 'string'; /** Options for write and writeFile */ -export interface WritingOptions extends CommonOptions { +export interface WritingOptions extends CommonOptions { /** Output data encoding */ - type?: 'base64' | 'binary' | 'buffer' | 'file' | 'array' | 'string'; + type: T; /** * Generate Shared String Table @@ -805,9 +835,11 @@ export interface Sheet2HTMLOpts { footer?: string; } -export interface Sheet2JSONOpts extends DateNFOption { +export type Sheet2JSONCellValue = string | number | boolean | DefaultVal; + +export interface Sheet2JSONOpts extends DateNFOption { /** Output format */ - header?: "A"|number|string[]; + header?: 'A' | 1 | readonly string[]; /** Override worksheet range */ range?: any; @@ -816,7 +848,7 @@ export interface Sheet2JSONOpts extends DateNFOption { blankrows?: boolean; /** Default value for null/undefined values */ - defval?: any; + defval?: DefaultVal; /** if true, return raw data; if false, return formatted text */ raw?: boolean; @@ -920,9 +952,12 @@ export interface XLSX$Utils { /* --- Export Functions --- */ /** Converts a worksheet object to an array of JSON objects */ + sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts & { header: readonly ColumnKey[] }): Record>[]; + sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts & { header?: undefined }): Record>[]; + sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts & { header: 'A' }): Record>[]; + sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts & { header: 1 }): Sheet2JSONCellValue[][]; + // ...or manually supply a row type sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts): T[]; - sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts): any[][]; - sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts): any[]; /** Generates delimiter-separated-values output */ sheet_to_csv(worksheet: WorkSheet, options?: Sheet2CSVOpts): string; diff --git a/types/xlsx-tests.ts b/types/xlsx-tests.ts index 2066970..22c9b94 100644 --- a/types/xlsx-tests.ts +++ b/types/xlsx-tests.ts @@ -74,3 +74,134 @@ const vbawb = XLSX.readFile("test.xlsm", {bookVBA:true}); if(vbawb.vbaraw) { const cfb: any /* XLSX.CFB.CFB$Container */ = CFB.read(vbawb.vbaraw, {type: "buffer"}); } + +// read +{ + XLSX.read(new Uint8Array(0)); + XLSX.read(new ArrayBuffer(0)); + + XLSX.read(new Uint8Array(0), { type: 'buffer' }); + XLSX.read(new ArrayBuffer(0), { type: 'array' }); + + XLSX.read('', { type: 'base64' }); + + // throw at runtime as the path is empty, but type checking should succeed + assertThrows(() => XLSX.read('', { type: 'file' })); + XLSX.read('', { type: 'string' }); + XLSX.read('', { type: 'binary' }); + + // @ts-expect-error plain object can't be parsed + XLSX.read({}); + // @ts-expect-error array can't be parsed + XLSX.read([]); +} + +// write +{ + const wb = XLSX.read(new Uint8Array(0)); + + const u8 = XLSX.write(wb, { type: 'buffer' }); + const arrayBuffer = XLSX.write(wb, { type: 'array' }); + const b64 = XLSX.write(wb, { type: 'base64' }); + + assert(u8 instanceof Uint8Array); + assert(arrayBuffer instanceof ArrayBuffer); + assert(typeof b64 === 'string'); + + // @ts-expect-error this will fail both at compile time and runtime without `opts.type` + assertThrows(() => XLSX.write(wb)); + // @ts-expect-error this will fail both at compile time and runtime without `opts.type` + assertThrows(() => XLSX.write(wb, {})); +} + +// sheet_to_json (compile-time type checking only) +{ + const wb = XLSX.read(new Uint8Array(0)); + const ws = wb.Sheets[wb.SheetNames[0]]; + + const _1 = XLSX.utils.sheet_to_json(ws, { header: 1 }); + _1[0] = [1, undefined, 'str', true]; + + const _a = XLSX.utils.sheet_to_json(ws, { header: ['a', 'b', 'c', 'd'] }); + _a[0] = { a: 1, b: 2, c: 3, d: undefined }; + // @ts-expect-error unrecognized key `e` + _a[0] = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + // @ts-expect-error missing `d` + _a[0] = { a: 1, b: 2, c: 3 }; + + const header2 = ['a', 'b', 'c', 'd'] as const; + const _a2 = XLSX.utils.sheet_to_json(ws, { header: header2 }); + _a2[0] = { a: 1, b: 2, c: 3, d: undefined }; + // @ts-expect-error unrecognized key `e` + _a2[0] = { a: 1, b: 2, c: 3, d: 4, e: 5 }; + // @ts-expect-error missing `d` + _a2[0] = { a: 1, b: 2, c: 3 }; + + const header3 = ['a', 'b', 'c', 'd']; + const _a3 = XLSX.utils.sheet_to_json(ws, { header: header3 }); + _a3[0] = { key: 'val' }; + // @ts-expect-error array not object + _a3[0] = ['val']; + + const _A = XLSX.utils.sheet_to_json(ws, { header: 'A' }); + _A[0] = { key: 'val' }; + // @ts-expect-error array not object + _A[0] = ['val']; + + const _u = XLSX.utils.sheet_to_json(ws, { header: undefined }); + _u[0] = { key: 'val' }; + // @ts-expect-error array not object + _u[0] = ['val']; + + const _u2 = XLSX.utils.sheet_to_json(ws); + _u2[0] = { key: 'val' }; + // @ts-expect-error array not object + _u2[0] = ['val']; + + const _u3 = XLSX.utils.sheet_to_json(ws, {}); + _u3[0] = { key: 'val' }; + // @ts-expect-error array not object + _u3[0] = ['val']; + + // manually supplying a type acts as a type assertion + const _x = XLSX.utils.sheet_to_json<[string, number]>(ws); + _x[0] = ['str', 1]; + // @ts-expect-error wrong column order + _x[0] = [1, 'str']; + + const _defVal = XLSX.utils.sheet_to_json(ws, { defval: new Date(0) }); + _defVal[0] = { key: new Date(0), key2: 'str' }; + // @ts-ignore - this will only error when strict null checking enabled `undefined` not allowed + _defVal[0] = { key: undefined }; + + const _defVal2 = XLSX.utils.sheet_to_json(ws, { header: 1, defval: new Date(0) }); + _defVal2[0] = [new Date(0), 'str']; + // @ts-ignore - this will only error when strict null checking enabled `undefined` not allowed + _defVal2[0] = [new Date(0), 'str', undefined]; + + const _defVal3 = XLSX.utils.sheet_to_json(ws, { header: ['a', 'b'], defval: new Date(0) }); + _defVal3[0] = { a: new Date(0), b: 'str' }; + // @ts-expect-error unrecognized key `c` + _defVal3[0] = { a: new Date(0), b: 2, c: new Date(0) }; + // @ts-expect-error missing `b` + _defVal3[0] = { a: 1 }; + // @ts-expect-error `undefined` not allowed + _defVal3[0] = { a: undefined }; +} + +function assert(condition: boolean, message?: string) { + if (!condition) { + throw new Error(message ?? 'condition returned false'); + } +} + +function assertThrows(fn: (() => any), message?: string) { + let thrown = false; + try { + fn(); + } catch { + thrown = true; + } + + if (!thrown) throw new Error(message ?? 'function failed to throw'); +}