Skip to content

Commit 072d6f5

Browse files
authored
Merge pull request #142 from tomoto/feat/infer-result-type-with-typescript
feat: infer result type correctly with TypeScript
2 parents afd78c2 + 5c8f9fa commit 072d6f5

File tree

4 files changed

+94
-7
lines changed

4 files changed

+94
-7
lines changed

filesize.d.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
// Type definitions for filesize 6.0.1
1+
// Type definitions for filesize 8.0.3
22
// Project: https://github.com/avoidwork/filesize.js, https://filesizejs.com
33
// Definitions by: Giedrius Grabauskas <https://github.com/GiedriusGrabauskas>
44
// Renaud Chaput <https://github.com/renchap>
55
// Roman Nuritdinov <https://github.com/Ky6uk>
66
// Sam Hulick <https://github.com/ffxsam>
7-
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
7+
// Tomoto S. Washio <https://github.com/tomoto>
88

99
declare var fileSize: Filesize.Filesize;
1010
export = fileSize;
@@ -100,8 +100,24 @@ declare namespace Filesize {
100100
roundingMethod?: "round" | "floor" | "ceil";
101101
}
102102

103+
// Result type inference from the output option
104+
interface ResultTypeMap {
105+
array: [number, string];
106+
exponent: number;
107+
object: {
108+
value: number,
109+
symbol: string,
110+
exponent: number,
111+
unit: string,
112+
};
113+
string: string;
114+
}
115+
type DefaultOutput<O extends Options> = Exclude<O["output"], keyof ResultTypeMap> extends never ? never : "string"
116+
type CanonicalOutput<O extends Options> = Extract<O["output"], keyof ResultTypeMap> | DefaultOutput<O>
117+
103118
interface Filesize {
104-
(bytes: number, options?: Options): string;
105-
partial: (options: Options) => ((bytes: number) => string);
119+
(bytes: number): string;
120+
<O extends Options>(bytes: number, options: O): ResultTypeMap[CanonicalOutput<O>];
121+
partial: <O extends Options>(options: O) => ((bytes: number) => ResultTypeMap[CanonicalOutput<O>]);
106122
}
107123
}

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
"build": "rollup -c",
2828
"watch": "rollup -c -w",
2929
"changelog": "auto-changelog -p",
30-
"test": "npm run build && npm run lint && npm run test:unit",
30+
"test": "npm run build && npm run lint && npm run test:unit && npm run test:type",
3131
"test:unit": "nodeunit test/*.js",
32-
"lint": "eslint test/*.js src/*.js",
33-
"types": "npx typescript src/filesize.js --declaration --allowJs --emitDeclarationOnly --outDir ./"
32+
"test:type": "npx tsc -p test",
33+
"lint": "eslint test/*.js src/*.js"
3434
},
3535
"devDependencies": {
3636
"@babel/core": "^7.15.5",

test/tsconfig.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"noEmit": true,
4+
"esModuleInterop": true,
5+
"strictNullChecks": true,
6+
"noImplicitAny": true,
7+
},
8+
"include": [
9+
"*.ts"
10+
]
11+
}

test/typeinference_test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// This "test" is to validate the type inference, i.e. only to compile, but not to run.
2+
3+
import filesize from "../filesize";
4+
5+
//
6+
// check functions for compilation time, no need to implement
7+
//
8+
9+
function shouldBeString(x: string) {}
10+
function shouldBeNumberUnitPair(x: [number, string]) {}
11+
function shouldBeNumber(x: number) {}
12+
13+
type FilesizeObject = {
14+
value: number,
15+
symbol: string,
16+
exponent: number,
17+
unit: string,
18+
}
19+
function shouldBeObject(x: FilesizeObject) {}
20+
21+
//
22+
// single possibility (typical)
23+
//
24+
25+
// direct call
26+
shouldBeString(filesize(123));
27+
shouldBeString(filesize(123, {}));
28+
shouldBeString(filesize(123, { output: undefined }));
29+
shouldBeString(filesize(123, { output: "string" }));
30+
shouldBeNumberUnitPair(filesize(123, { output: "array" }));
31+
shouldBeNumber(filesize(123, { output: "exponent" }));
32+
shouldBeObject(filesize(123, { output: "object" }));
33+
34+
// partial
35+
shouldBeString(filesize.partial({})(123))
36+
shouldBeString(filesize.partial({ output: "string" })(123))
37+
shouldBeNumberUnitPair(filesize.partial({ output: "array" })(123))
38+
shouldBeNumber(filesize.partial({ output: "exponent" })(123))
39+
shouldBeObject(filesize.partial({ output: "object" })(123))
40+
41+
//
42+
// mutliple possibilities (tricky)
43+
//
44+
45+
let opt1!: { output: "string" | "array" };
46+
const result1 = filesize(123, opt1); // string | [number, string]
47+
result1 as string
48+
result1 as [number, string];
49+
50+
let opt2!: { output: "exponent" | "object" };
51+
const result2 = filesize(123, opt2); // number | { ... }
52+
result2 as number
53+
result2 as FilesizeObject
54+
55+
// Note: strictNullChecks needs to be true to correctly handle this scenario.
56+
// If false, the compiler cannot know the return type may be string.
57+
let opt3!: { output?: "exponent" };
58+
const result3 = filesize(123, opt3); // string | number
59+
result3 as number
60+
result3 as string

0 commit comments

Comments
 (0)