Open artalar opened 4 years ago
As i understood it should be something like this:
const data: $Unknown<Array<number>> = JSON.parse(some)
function head(list: $Unknown<Array<string>>, fallback: string = ''): string {
if (typeof list == 'number') // Type $Unknown<Array<string>> can't be "number" type
{
}
if (list instanceof Array) {
// Expected: unknown
const item = list[0]
if (typeof item == 'string')
{
return item
}
}
return fallback
}
// Expected: Type "Array<number>" is incompatible with type "Array<string>"
// Actual: OK!
const result = head(data)
IMO T | unknown
is just unknown
. So both Array<string> | unknown
and Array<number> | unknown
are just unknown
and therefore they are assignable to each other.
@raveclassic The behavior is needed for the try..catch
statement. To show, that in catch may be placed as well unknown
and your own defined errors from called functions.
How is that different? Looking at example from the docs:
function assertIsTrue(arg) {
if (!arg) {
throw new TypeError("arg is invalid")
}
}
try {
assertIsTrue(false);
} catch (e) {
// Type of "e" variable is "TypeError | unknown"
}
it's not clear to me how should I work with e
in the catch
block and what advantages does | unknown
provide in the type?
In TS error is usually checked with:
if (e instanceof TypeError) {
// e is `TypeError`
} else {
// e is unknown
}
But it's absolutely the same for catch(e: unknown)
without that TypeError |
part.
Keeping that in mind I would expect the error to be plain e: TypeError
without unknown
because assertIsTrue
only throws TypeError
. That would make sense.
The difference is in the fast understanding of which errors can be caught by the try..catch
block.
If you will see only unknown
type, it gives you nothing, but, if you see that it will be UserNotFoundError | DataBaseError | unknown
you can understand which errors you can catch, and find some problems related to that.
As an example (UserNotFoundError | DataBaseError | unknown
), you can understand, that you call method/function which throws low-level error DataBaseError
, and you can find it and rethrow DataBaseError
as other top-level error.
Seems we're talking about different things. I'm absolutely ok with declaring throwed errors in function signature (this reminds me checked exceptions in Java). What I don't get is:
unknown
added to the unionunknown
in the most general case. Seems Hegel encodes unknown
in a different way than TypeScript. If so, could you please clarify why is T | unknown
not equal to unknown
?
I understand your position and also I agree with that. (Actually unknown | T
should be unknown
). But I don't know which way is better for informative hints in try..catch
block. If you have some advice or interesting solution for the case - I will the first one who change the semantic of union with unknown
type 😄
That's a very good question. I'm pretty sure I don't see the whole picture but at least I'd suggest not using unknown
in unions because it ultimately defeats the purpose of such unions for a person not familiar with these highly specific semantics.
Back to the original issue: if we both agree that adding unknown
to a type (not in try/catch case) effectively turns such type to unknown
, then I'd suggest using tuples+spreads (I don't know if Hegel supports them ATM):
declare const head: <A>(list: [A, ...unknown[]]) => A
Ok, here another case. You write a library for npm and type an argument of public function. You can't control a user env and don't know which type system they used (or no one at all). So u in same time want to describe a type of argument and what to mark it as unknown for internal contract provement. How to describe it better in code? May be we need a two kindest type: with two different signature, for publick and internal usage. Because if mark an argument of a function as generic unknown we tell a user of function that they may to provide anything by argument too, which is wrong.
I think we need a more investigation for this...
In TS I'd just declare it as unknown
because it's the "smallest" possible type for a function argument, I mean when checking this argument in the body of that function. While also the "widest" type to pass to that function from outside. This reflects how outer-world interop can be encoded.
Even if you add something more specific to the union you can't guarantee that in runtime the argument will adhere the type since Hegel is just a typechecker.
@raveclassic do you really think it is safety??
A type system, with this case, is useless and getting false garanties
I don't get the example. You declare a function which returns any
(ehw), and then redefine that function. Could you clarify what do you mean?
Here's how I see it:
// this code requires runtime checks to guarantee that calls to `foreign` always return `number`
export declare const foreign: (data: string) => number;
export function process(): number {
const foo = foreign('foo');
const bar = foreign('bar');
return foo + bar; // UNSAFE!
}
export declare const safeForeign: (data: string) => unknown;
// note the `| undefined` here
export function safeProcess(): number | undefined {
const foo = safeForeign('foo');
const bar = safeForeign('bar');
// return foo + bar; // Error! Object is of type `unknown`
// we need to check
if (typeof foo === 'number' && typeof bar === 'number') {
return foo + bar;
}
// here, foo and bar are still of type `unknown`
}
A problem: we can't define a expected type for future refinement:
If we define a
head
as function thats accept onlyArray<number>
withoutunknown
- we need to provide a type-garanteddata
for that function, thats may be an overhead for current case (walk of all array elements to refine it type).I expecting thats
unknown
type may be a generic:unknown</* meta type: */Array<number>>
.As an option we could review a workaround: unknown union should not lose an other not unknown types. In other words the top ^ example just should work as a expected comment, but I think it is not intuitive because
unknown
as a part of union, technically, may be used for any refine, thats superfluously (не знаю как слова подобрать - кароч мы должны пресекать возможность передавать параметризированный unknown в функции принимающие простой unknown). May be, as a solution, we should use intersection for expected unknown:MyType & unknown
, but I think container type is will be better:$Unknown<MyType>
.