Issue
I have the following hash
function that return a string
when encoding
argument is passed. Otherwise, it will return a Buffer
,
import { createHash, BinaryToTextEncoding } from "crypto";
const hash: {
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
} = (data: string | Buffer, encoding?: BinaryToTextEncoding) => {
const hash = createHash("sha256");
hash.update(data);
if (encoding) {
// Hash.digest(encoding: BinaryToTextEncoding): string (+1 overload)
return hash.digest(encoding) as any;
}
// Hash.digest(): Buffer (+1 overload)
return hash.digest();
};
the above function is working as expected,
const hashed = hash("hello", "hex"); // const hashed: string
// or
const hashed = hash("hello"); // const hashed: Buffer
but, what is not clear to me is I have to define as any
while the hash.digest(encoding)
is returning a string
,
if (encoding) {
// Hash.digest(encoding: BinaryToTextEncoding): string (+1 overload)
return hash.digest(encoding) as any;
}
TypeScript will give an error without the above as any
:
Type '(data: string | Buffer, encoding?: BinaryToTextEncoding) => string | Buffer' is not assignable to type '{ (data: string | Buffer): Buffer; (data: string | Buffer, encoding: BinaryToTextEncoding): string; }'.
Type 'string | Buffer' is not assignable to type 'Buffer'.
Type 'string' is not assignable to type 'Buffer'.ts(2322)
const hash: {
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
}
Do you have any idea?
Solution
The implementation of an overloaded function with multiple call signatures is never properly type checked in TypeScript. The standard way of writing an overload is with a function
statement like
function hash(data: string | Buffer): Buffer;
function hash(data: string | Buffer, encoding: BinaryToTextEncoding): string;
function hash(data: string | Buffer, encoding?: BinaryToTextEncoding) {
const hash = createHash("sha256");
hash.update(data);
if (encoding) {
return hash.digest(encoding); // okay
}
return hash.digest(); // okay
};
in which you don't have to write a type assertion like as any
. This compiles without error, but that's just because the compiler is only loosely checking the implementation against the call signatures. As long as you return something that looks like "a string
or a Buffer
" the compiler will accept it, even if it has nothing to do with the encoding
parameter:
function badHash(data: string | Buffer): Buffer;
function badHash(data: string | Buffer, encoding: BinaryToTextEncoding): string;
function badHash(data: string | Buffer, encoding?: BinaryToTextEncoding) {
return data; // also okay, even though this is not safe
};
You can read about this at microsoft/TypeScript#13235. It would be too complex for the compiler to analyze the function implementation multiple times to make sure each call signature is separately satisfied. So for function statements it errs on the side of permissiveness, leading to unsoundness.
On the other hand, when you have a function expression like an arrow function, you have the opposite problem. The compiler still doesn't analyze the implementation once per each call signature; instead it checks the implementation too strictly, only accepting it if the return type would work for all the call signatures. For function expressions, it errs on the side of restrictiveness, leading to incompleteness.
So unless your function somehow returned both a string
and a Buffer
(e.g., the intersection type string & Buffer
, it complains:
const hash: { // error! string | Buffer not assignable to Buffer
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
} = (data: string | Buffer, encoding?: BinaryToTextEncoding) => {
const hash = createHash("sha256");
hash.update(data);
if (encoding) {
return hash.digest(encoding);
}
return hash.digest();
};
You can read about this issue at microsoft/TypeScript#47669. The suggestion there is for overloaded function expressions to be checked the same loose way as overloaded function statements. But for now it's not part of the language and you need something like a type assertion, or some other type safety loosening.
If you assert that each value returned is the intersection string & Buffer
then it will compile without error (even though it is essentially impossible for something to be both a string
and a Buffer
):
const hash: {
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
} = (data: string | Buffer, encoding?: BinaryToTextEncoding) => {
const hash = createHash("sha256");
hash.update(data);
if (encoding) {
return hash.digest(encoding) as (string & Buffer); // okay
}
return hash.digest() as (string & Buffer); // okay
};
Or you could use a single as any
assertion, which, due to the "contagious" nature of the intentionally unsafe any
type, causes the whole return type to be any
, which is compatible with everything, including both string
and Buffer
:
const hash: {
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
} = (data: string | Buffer, encoding?: BinaryToTextEncoding) => {
const hash = createHash("sha256");
hash.update(data);
if (encoding) {
return hash.digest(encoding);
}
return hash.digest() as any; // either return statement will do
};
Or you could just annotate the return value as being any
without using an assertion (which is only accepted because any
is unsafe and allowed to be assignable both to and from just about any other type):
const hash: {
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
} = (data: string | Buffer, encoding?: BinaryToTextEncoding): any => { // here
const hash = createHash("sha256");
hash.update(data);
if (encoding) {
return hash.digest(encoding);
}
return hash.digest()
};
Any of these approaches will "work" in the sense that the compiler stops complaining. But none of them are safe. Indeed, using any
makes it even easier for you to mess up the implementation without a hope of it being caught for you:
const hash: {
(data: string | Buffer): Buffer;
(data: string | Buffer, encoding: BinaryToTextEncoding): string;
} = (data: string | Buffer, encoding?: BinaryToTextEncoding): any => {
return 123; // also acceptable because of any
};
Again, overloaded function implementations are never really checked properly. You can choose between false negatives, where the compiler fails to catch real errors, or false positives, where the compiler complains about errors that don't really exist... and fixing those false positives just means you open up the possibility for false negatives again. So it's up to you which approach you take.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.