The any
type is more permissive than is desired in many circumstances. The problem is that any
implicitly conforms to all possible interfaces; since no object actually conforms to every possible interface, any
is implicitly type-unsafe. Using any
requires type-checking it manually; however, this checking is easy to forget or mess up. Ideally we'd want a type that conforms to {}
but which can be refined to any interface via checking.
I'll refer to this proposed type as unknown
. The point of unknown
is that it does not conform to any interface but refines to any interface. At the simplest, type casting can be used to convert unknown
to any interface. All properties/indices on unknown
are implicitly treated as unknown
unless refined.
The unknown
type becomes a good type to use for _untrusted_ data, e.g. data which _could_ match an interface but we aren't yet sure if it does. This is opposed to any
which is good for _trusted_ data, e.g. data which could match an interface and we're comfortable assuming that to be true. Where any
is the escape hatch out of the type system, unknown
is the well-guarded and regulated entrance into the type system.
(edit) Quick clarification: https://github.com/Microsoft/TypeScript/issues/10715#issuecomment-359551672
e.g.,
let a = JSON.parse(); // a is unknown
if (Arary.isArray(a.foo)) ; // a is {foo: Array<unknown>} extends unknown
if (a.bar instanceof Bar) // a is {bar: Bar} extends unknown
let b = String(a.s); // b is string
a as MyInterface; // a implements MyInterface
Very roughly, unknown
is equivalent to the pseudo-interface:
pseudo_interface unknown extends null|undefined|Object {
[key: any]: unknown // this[?] is unknown
[any]: unknown // this.? is unknown
[key: any](): ()=>unknown // this.?() returns unknown
}
I'm fairly certain that TypeScript's type model will need some rather large updates to handle the primary cases well, e.g. understanding that a type is freely refinable but not implicitly castable, or worse understanding that a type may have non-writeable properties and allowing refinement to being writeable (it probably makes a lot of sense to treat unknown
as immutable at first).
A use case is user-input from a file or Web service. There might well be an _expected_ interface, but we don't at first know that the data conforms. We currently have two options:
1) Use the any
type here. This is done with the JSON.parse
return value for example. The compiler is totally unable to help detect bugs where we pass the user data along without checking it first.
2) Use the Object
type here. This stops us from just passing the data along unknown, but getting it into the proper shape is somewhat cumbersome. Simple type casts fail because the compiler assumes any refinement is impossible.
Neither of these is great. Here's a simplified example of a real bug:
interface AccountData {
id: number;
}
function reifyAccount(data: AccountData);
function readUserInput(): any;
const data = readUserInput(); // data is any
reifyAccount(data); // oops, what if data doesn't have .id or it's not a number?
The version using Object
is cumbersome:
function readUserInput(): Object;
const data = readUserInput();
reifyAccount(data); // compile error - GOOD!
if (data as AccountData) // compile error - debatably good or cumbersome
reifyAccount(data);
if (typeof data.id === 'number') // compile error - not helpful
reifyAccount(data as AccountInfo);
if (typeof (data as any).id === 'number') // CUMBERSOME and error-prone
reifyAccount((data as any) as AccountInfo); // still CUMBERSOME and error-prone
With the proposed unknown
type;
function readUserInput(): unknown;
const data = readUserInput(); // data is unknown
if (typeof data.id === 'number') // compiles - GOOD - refines data to {id: number} extends unknown
reifyAccount(data); // data matches {id: number} aka AccountData - SAFE
The idea of introducing a property in the refined type after offering proof of its existence and type in a type guard is definitely interesting. Since you didn't mention it I wanted to point out that you _can_ do it through user-defined type guards, but it obviously takes more typing than your last example:
interface AccountData {
id: number;
}
function isAccountData(obj: any): obj is AccountData {
return typeof obj.id === "number";
}
declare function reifyAccount(data: AccountData): void;
declare function readUserInput(): Object;
const data = readUserInput(); // data is Object
if (isAccountData(data)) {
reifyAccount(data); // data is AccountData
}
The advantage to this approach is that you can have any sort of logic you want in the user-defined type guard. Often such code only checks for a few properties and then takes it as proof that the type conforms to a larger interface.
I like the idea of differentiating "trust me, I know what I'm doing" from "I don't know what this is, but I still want to be safe". That distinction is helpful in localizing unsafe work.
For anyone interested, there's a good deal of related discussion about the pros/cons of any
and {}
in #9999. The desire for a distinct unknown
type is mentioned there, but I really like the way @seanmiddleditch has presented it here. I think this captures it brilliantly:
Where
any
is the escape hatch out of the type system,unknown
is the well-guarded and regulated entrance into the type system.
Being able to express a clear distinction between _trusted_ (any
) and _untrusted_ (unknown
) data I think could lead to safer coding and clearer intent. I'd certainly use this.
I'll also point out that some very strongly typed languages still have an escape hatch bottom type themselves for prototyping (e.g. Haskell's undefined
, Scala's Nothing
), but they still have a guarded entrance (Haskell's forall a. a
type, Scala's Any
). In a sense, any
is TypeScript's bottom type, while {} | void
or {} | null | undefined
(the type of unknown
in this proposal) is TypeScript's top type.
I think the biggest source of confusion is that most languages name their top type based on what extends it (everything extends Scala's Any
, but nothing extends Scala's Nothing
), but TypeScript names it based on what it can assign to (TypeScript's any
assigns to thing, but TypeScript's {} | void
only assigns to {} | void
).
@isiahmeadows any
is universally assignable both _to_ and _from_ all other types, which in the usual type parlance would make it both a top type _and_ a bottom type. But if we think of a type as holding a set of values, and assignability only being allowed from subsets to supersets, then any
is an impossible beast.
I prefer to think of any
more like a compiler directive that can appear in a type position that just means 'escape hatch - don't type check here'. If we think of any
in terms of it's type-theory qualities, it just leads to contradictions. any
is a type only _by definition_, in the sense that the spec says it is a type, and says that it is assignable to/from all other types.
Okay. I see now. So never
is the bottom type. I forgot about any
being
there for supporting incremental typing.
On Mon, Sep 5, 2016, 23:04 yortus [email protected] wrote:
@isiahmeadows https://github.com/isiahmeadows any is universally
assignable both _to_ and _from_ all other types, which in the usual type
parlance would make it both a top type _and_ a bottom type. But if we
think of a type as holding a set of values, and assignability only being
allowed from subsets to supersets, then any is an impossible beast.I prefer to think of any more like a compiler directive that can appear
in a type position that just means 'escape hatch - don't type check here'.
If we think of any in terms of it's type-theory qualities, it just leads
to contradictions. any is a type only _by definition_, in the sense that
the spec says it is a type, and says that it is assignable to/from all
other types.β
You are receiving this because you were mentioned.Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/10715#issuecomment-244839425,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBC3YMJnttuqXk4Dq6iTh9VC7orY2ks5qnNg7gaJpZM4J1Wzb
.
This could use clarification with some more examples -- it's not clear from the example what the difference between this type and any
are. For example, what's legal to do with any
that would be illegal to do with unknown
?
With {}
, any
, {} | null | undefined
, the proposed but unimplemented object
, and never
, most use cases seem to be well-covered already. A proposal should outline what those use cases are and how the existing types fail to meet the needs of those cases.
@RyanCavanaugh
If I understand correctly:
let x;
declare function sendNumber(num: number);
sendNumber(x); // legal in any
sendNumber(x); // illegal in unknown and {}
if (typeof x.num === "number") {
sendNumber(x.num); // legal in any and unknown, illegal in {}
}
BTW, what does the proposed-but-unimplemented object
type do? I haven't seen or read about it.
@SaschaNaz Your understanding matches mine, too.
declare function send(x: number)
let value: unknown
send(value) // error
send(value as any) // ok
if (typeof value === "number") {
send(value) // ok
}
On Wed, Sep 28, 2016, 19:11 Kagami Sascha Rosylight <
[email protected]> wrote:
@RyanCavanaugh https://github.com/RyanCavanaugh
If I understand correctly:
let x;declare function sendNumber(num: number);
sendNumber(x); // legal in any
sendNumber(x); // illegal in unknown and {}
if (typeof x.num === "number") {
sendNumber(x.num); // legal in any and unknown, illegal in {}
}β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/10715#issuecomment-250327981,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBO-yD4Qdc605NubmW6yKrBXH5ZFTks5quvQUgaJpZM4J1Wzb
.
I think the request here is for unknown
to be { } | null | undefined
, but be allowed to "evolve" as you assert/assign into it.
Suggestion: drop unknown
keyword, and apply "the ability to evolve" feature to {}
itself.
That would limit cognitive load and proliferation of 'exception' types like never
, unknown
, any
, void
etc. But also it would force people to spell null-ability when it comes to the alien data.
The currently existing {}
type, and intersection with it will get an extra feature, being "evolvable".
Evolvable means you can probe its properties liberally without casting. Probing means accessing property in special known positions:
var bla: {};
if (bla.price) console.log("It's priced!"); // GOOD, we **probed** price
console.log(bla.price); // BAD, we're **using** price, which isn't safe
if (bla.price) console.log(bla.price); // GOOD, we **probed**, then we can use
Probing works very similar to type assertions, and in a conventional JS coding style too. After a property is probed, the type of the expression changes to {} & { property: any; }
, allowing immediate use of the property as in the last line of the example above.
I suggest these three supported ways of probing:
// non-null probing, asserts {} & { property: any; }
if (bla.price) console.log("priced!");
// property probing, asserts {} & { property: any | null | undefined }
if ('price' in bla) console.log("priced!");undefined; }
// typed probing, asserts {} & { property: type; }
if (typeof bla.price==='number') console.log("priced!");}
// custom assert probing, asserts {} & { property: type; }
if (isFinite(bla.price)) console.log("priced!");
It's crucial to allow "evolability" to more than just one selected type, but intersections too. Consider multi-property asserts that naturally come out of it:
if (bla.price && bla.articleId && bla.completed)
acknowledgeOrder(bla);
Lastly I want to highlight the real danger of unknown
keyword:
unknown
undefined
Those two are way too similar, and be confused in all sorts of situations. Mere typos would be a big problem in itself. But factor in genuine long-term misconceptions this similarity would inevitably breed.
Picking another, less similar keyword might help, but going straight for an existing syntax is much better.
The point of {}
in the first place is to mark values we don't know properties of. It's not for objects without properties, it's objects with unknown properties. Nobody really uses empty objects except in some weirdo cases.
So this extra sugar added on {}
would most of the time be a welcome useful addition right where it's helpful. If you deal with unknown-properties case, you get that probing/assertion evolvability intuitive and ready. Neat?
UPDATE: replaced unions with intersections up across the text, my mistake using wrong one.*
I think changing existing behavior is too surprising.
let o1 = {};
o1.foo // okay
let o2 = { bar: true };
o1.foo // suddenly not okay :/
No, the first is not OK either β you're not probing there (for non-null probing it would require a boolean-bound position to qualify).
With the probing definitions outlined above, compiler still errors on genuine errors, but it would handle probing/evolving neatly without excessive verbosity.
Also note that {} naturally fits with strictNullChecks
story β and with my suggestions it continues to do so neatly. Meaning it follows stricter checks when option is enabled, and gets relaxed when it isn't.
Not necessary the case with unknown
:
var x: unknown;
x = null // is it an error? you would struggle to guess, i.e. it impedes readability
var x: {};
x = null; // here the rules are well-known
I really don't like this. It removes a level of type safety in the
language, and it would especially show up when you have large numbers of
boolean flags on an object. If you change the name of one, you might miss
one and TypeScript wouldn't tell you that you did, because it's just
assuming you're trying to narrow the type instead.
On Wed, Oct 26, 2016, 08:52 mihailik [email protected] wrote:
Suggestion: drop unknown keyword, and apply "the ability to evolve"
feature to {} itself.That would limit cognitive load and proliferation of 'exception' types
like never, unknown, any, void etc. But also it would force people to
spell null-ability when it comes to the alien data.
EvolvabilityThe currently existing {} type, and union with it will get an extra
feature, being "evolvable".Evolvable means you can probe its properties liberally without casting.
Probing means accessing property in special known positions:var bla: {};if (bla.price) console.log("It's priced!"); // GOOD, we probed price
console.log(bla.price); // BAD, we're using price, which isn't safeif (bla.price) console.log(bla.price); // GOOD, we probed, then we can useProbing works very similar to type assertions, and in a conventional JS
coding style too. After a property is probed, the type of the expression
changes to {} | { property: any; }, allowing immediate use of the
property as in the last line of the example above.I suggest these three supported ways of probing:
// non-null probing, asserts {} | { property: any; }if (bla.price) console.log("priced!");if ('price' in bla) console.log("priced!"); // property probing, asserts {} | { property: any | null | undefined; }if (typeof bla.price==='number') console.log("priced!"); // typed probing, asserts {} { property: type; }if (isFinite(bla.price)) console.log("priced!"); // custom assert probing, asserts {} { property: type; }
β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/10715#issuecomment-256337526,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBDdp6AGkgsZTHdn8g0pzWMBdS1-gks5q300RgaJpZM4J1Wzb
.
I think you're missing a point @isiahmeadows β "evolvability" is only enabled for {}
and its intersections.
Most normal types won't have that feature: number
, Array
, MyCustomType
, or even { key: string; value: number; }
are not "evolvable".
If you have an example with boolean flags, let's see how it works.
A top type like this unknown
would be immensely useful for type safety reasons. It gets old typing {} | void
each time.
I would really like to see this made a reality. It seems like the type should be
type unknown = {} | void | null
to truly be at the top of the type lattice. I would've thought
type unknown = {} | undefined | null
would be sufficient, but void
is not assignable to the latter for reasons which I don't understand. I opened an issue to clarify this at #20006.
For those interested there is a microlibrary for this type in the meantime, although the definition there seems more complicated than it needs to be.
Have not seen it mentioned here, but this proposal looks quite similar to the mixed
type in Flow.
@pelotom Try type unknown = {} | void | undefined | null
. Also, in my experience, void
is kind of a supertype of sorts of undefined | null
, and I usually use void
instead of the union for that reason. I've also never experienced () => [1, 2, 3]
as being assignable to () => void
.
@isiahmeadows
Also, in my experience,
void
is kind of a supertype of sorts ofundefined | null
, and I usually usevoid
instead of the union for that reason.
undefined
is assignable to void
, but null
is not, when using --strictNullTypes
(i.e. null
is no more assignable to void
than to any other type).
I've also never experienced () => [1, 2, 3] as being assignable to () => void.
Try it:
const f: () => void = () => [1, 2, 3]
Long and short of the notes from #20284: What (if anything) should make unknown
different from a type alias for {} | null | undefined
?
@RyanCavanaugh
var x: unknown;
x.x // should still be unknown, but error on `{} | null | undefined`
@RyanCavanaugh
Long and short of the notes from #20284: What (if anything) should make unknown different from a type alias for
{} | null | undefined
?
No difference that I know of.
@saschanaz
var x: unknown;
x.x // should still be unknown, but error on{} | null | undefined
What? We already have that, it's called any
.
What? We already have that, it's called any.
Then the x.x
becomes any
instead of unknown
which has a different behavior.
Then the
x.x
becomesany
instead ofunknown
which has different behavior.
If arbitrary properties of x: unknown
can be accessed and have type unknown
themselves, that's exactly the behavior of any
, which is too permissive. It's just any
by a different name.
Is x.x(3)
valid (returns unknown
) ?
If arbitrary properties of x: unknown can be accessed and have type unknown themselves, that's exactly the behavior of any
The OP does not describe this explicitly but this is what I understand: https://github.com/Microsoft/TypeScript/issues/10715#issuecomment-250327981 (which is not the behavior of any
).
Is x.x(3) valid (returns unknown) ?
The proposal from the OP says yes. (But I'm starting to think it is too permissive?)
Very roughly, unknown is equivalent to the pseudo-interface: pseudo_interface unknown extends null|undefined|Object { [key: any]: unknown // this[?] is unknown [any]: unknown // this.? is unknown [key: any](): ()=>unknown // this.?() returns unknown }
I would see the usage of unknown wrt to member retrieval as requiring a type guard before the retrieval is valid.
anyFoo.bar; // ok
unknownFoo.bar // error
if (unknownFoo.bar) unknownFoo.bar // ok
Is
x.x(3)
valid (returnsunknown
) ?
No. If x: unknown
, you can't do anything with it besides pass it around, or use a type guard or predicate on it to narrow its type to something usable.
We already have type guards/predicates for the purpose of evolvability... that seems to me an orthogonal concern from having a standardized strict top type, which is what I want from unknown
.
if (unknownFoo.bar) unknownFoo.bar // ok
if ('bar' in unknownFoo) unknownFoo.bar // ok
The if (unknownFoo.bar) unknownFoo.bar
syntax would be nice too, but again I think that is a different, orthogonal feature. You would want such a feature to work on other types besides unknown
, wouldn't you?
I think the idea from OP is this:
declare function foo(input: number): void;
declare var bar: number;
function processUnknown() {
var o: unknown; // we don't know its type...
o.baz // okay, but please be careful...
o.baz() // okay, use `o` like `any` in your *local* area.
foo(o); // NOOO you don't know the type of `o` so please please do not pass it!
bar = o; // No, DO NOT SPREAD the type unsafety
if (typeof o === "number") {
foo(o); // Yes!
}
}
Summary: Play with your unknown toy in your room, not in the entire house.
o.baz() // okay, use `o` like `any` in your *local* area. foo(o); // NOOO you don't know the type of `o` so please please do not pass it!
Why is the first any better than the second though? They're both completely unsafe.
I just want a proper top type. DefinitelyTyped is full of examples where any
is used as a top type, but that's obviously wrong because libraries should not be in a position to turn off type checking in client code, which is what any
does in output positions.
I'm not sure if this is what @RyanCavanaugh was referring to with the SBS comment 'Would hopefully remove a lot of confusion from DefinitelyTyped'.
If there was a simple top type unknown
, then that would be the obvious type to use in many scenarios that currently use any
, the difference being that it doesn't disable type-checking.
Why is the first any better than the second though?
An author can be sure that any unsafe object is not being passed around.
I just followed @marcind 's link above, and I think it's a nice and simple write-up of the case for unknown
(called mixed
there), and the different case for any
, which many mistake for the top type. It also mentions refinements as simply being what typescript calls type guards.
mixed
will accept any type of value. Strings, numbers, objects, functionsβ anything will work. [....] When you try to use a value of amixed
type you must first figure out what the actual type is [with a type guard] or youβll end up with an error.
any
typesIf you want a way to opt-out of using the type checker,
any
is the way to do it. Usingany
is completely unsafe, and should be avoided whenever possible.
By this definition, unknown
is equivalent to {} | undefined | null
, but that's fine with me if we can have a single named and documented top type that is not unsafe like any
.
An author can at least be sure that any unsafe object is _not_ being passed around.
A bug is a bug is a bug π€·ββοΈ The value of that half measure is not at all clear to me. Meanwhile the cost seems high: yet another magical non-type like any
and void
with its own peculiar semantics, a complexity multiplier on the type system. And after all that we still wouldn't have a true top type!
@pelotom what would be a true top type?
@marcind {} | undefined | null
Using any is completely unsafe, and should be avoided whenever possible.
I like this π
Sounds better to file a new issue for mixed
?
@saschanaz Flow's mixed
is basically this proposal's unknown
(or what it evolved to be in the comments, at least). The only difference is the name.
The proposal is more complicated, I think we don't need this behavior for mixed
.
pseudo_interface unknown extends null|undefined|Object {
[key: any]: unknown // this[?] is unknown
[any]: unknown // this.? is unknown
[key: any](): ()=>unknown // this.?() returns unknown
}
...which allows let o: unknown; o.anyArbitraryMember();
If it's time to start bikeshedding names...
mixed
is a dumb name IMO, not at all obvious what that meansunknown
is pretty good.. a little long and looks kinda like undefined
but w/ealways
is what I've called this in my own code, because it's dual to never
top
has the advantage of being both accurate and succinct π My searching skills are failing me, so can anyone point me where in the documentation/handbook (or the woefully outdated specification) it is described what {}
actually means. And how/if it's different from Object
(which is, of course, different from object
).
Perhaps it is (at least for the aspects I care about) a documentation problem?
mixed is a dumb name IMO. unknown is pretty good. I've also called this type always in my own code, because it's dual to never.
One advantage of copying flow's keyword would be more tooling support for free (or close to free). I'm working in a React project that uses flow and I find the VSCode typescript-based tooling works quite well in a lot of situations because the syntax is so similar. Obviously that should not be the driving decision, but something to consider.
To expand on the documentation issue, the syntax {} | undefined | void
does a poor job of conveying that something could be of any value but we're not throwing away type checking completely by using any
. Having an alias for that type union would at least provide something that users could search on.
Combining that with https://github.com/Microsoft/TypeScript/issues/10485 would go a long way towards removing the need to use any
.
@marcind
My searching skills are failing me, so can anyone point me where in the documentation/handbook (or the woefully outdated specification) it is described what
{}
actually means. And how/if it's different fromObject
(which is, of course, different fromobject
).
Object
should just be avoided.object
is for objects which are _not_ primitives.{}
is the type of an object about which nothing is known except that it's an object (i.e. not undefined
or null
). Thus {} | undefined | null
is the type of a _value_ about which nothing is known, not even that it's an object.Thanks @pelotom. Marius Schulz's blog is super helpful and I should've know to turn there for answers.
@saschanaz To clarify, I said "or what it evolved to be in the comments", since what everyone read it as and discussed was closer to that of Flow's mixed
rather than the glorified any
that's somehow disjoint from every other type.
For those interested, I've added unknown
to Type Zoo.
I think a huge improvement would already to make it possible to narrow any
with type guards.
@ahejlsberg this for example does not work:
const json = await resp.json()
if (Array.isArray(json.errors) && json.errors.length > 0) {
throw Object.assign(new Error(json.errors.map(e => e.message)), { errors: json.errors })
}
json.errors
is still any
after the Array.isArray()
check, instead of any[]
. So I could mistype .length
, I don't get autocompletion for json.errors.map()
etc.
Ack, so I disappear for a little while and this exploded. :)
Having now read (cough skimmed) the bazillion comments, I do think that Flow's mixed
is exactly what I'm after. I don't use or know Flow but the documentation indicates the behavior I'm seeking.
I agree that the subscripting behavior I originally spelled out is a bad idea as it's too permissive and defeats the purpose. For the use cases that involve an object with open subscripting, specifying a type of {[key: any]: unknown}
would work just fine; likewise for arrays of unknown type, {[index: number]: unknown}
.
If type unknown = {} | undefined | null
would work, that's totally fine by me. I'll give it a go from Type Zoo when I get a chance and see if it handles the various use cases I have handy. I can't think of any reasons right now why it wouldn't work and it's definitely the simple solution. :)
Assuming it works (and I think it will), adding some type aliases to the core library along the lines of unknown
would suffice for me. _Maybe_ also adding in aliases for {[key: any]: unknown}
and {[index: number]: unknown}
too. :)
@seanmiddleditch Mind making a quick edit to the original proposal to point to this comment? It might help people better understand what you're going for.
As for the second half, I doubt you'll find much support for the second part:
Assuming it works (and I think it will), adding some type aliases to the core library along the lines of unknown would suffice for me. Maybe also adding in aliases for
{[key: any]: unknown}
and{[index: number]: unknown}
too. :)
The reason being, this still works closer to any
(specifically a non-nullable one that requires explicit casts from trusted data) than a proper top type (which does not require explicit casts to treat trusted as untrusted).
Additionally, if you can't trust the type, how are you supposed to know it's even callable, or if it's even something you can access a property of? If you don't know anything about that type coming in, it could very well be null
. For example, here's the only guarantees you have about JSON.parse
's return type (and second reviver
callback argument):
Object.prototype
).JSON.parse
can itself.undefined
(although it can return null
).You can deduce one more thing: you can't get a function out of it.
This is why nobody wants those two extra overloads - they are a really bad idea for untrusted data. (If you don't trust it, you shouldn't try to call it either.)
Beyond simply adding an alias for unknown
, it'd be really nice if core APIs were modified to return unknown
where appropriate: e.g. JSON.parse
returning unknown
and Array.isArray(x)
would be a type guard for x is unknown[]
. (Rather than x is any[]
)
Obviously, those would be breaking changes, but perhaps that can be avoided by putting this can be put behind a strict-mode flag? If the flag was off, unknown
would be treated as any
, which would maintain backwards compatibility.
Without a flag, I think this'll be a nice "best-practice" utility to use in application code, but won't get much traction in libraries, which will likely not want to introduce a major backwards-compatibility break.
What would be nice if an option in the compiler could exist that would replace any "any" type from 3rd party declarations or generelly all typescript code with this new "unknown" type. Often some "any" types just slip in and you have no way of knowing, ever -- until a bug comes up. Any one with me on this?
Yes I want a compiler option to make me aware of any "any" occurances that I may be unknowingly using.
@neoncom
I want a compiler option to make me aware of any "any" occurances that I may be unknowingly using.
This tslint rule might help: https://palantir.github.io/tslint/rules/no-unsafe-any/
Additionally, if you can't trust the type, how are you supposed to know it's even callable
I'm not sure I follow your example or reasoning.
An example use of {[key: any]: unknown}
would be the Request.body
object in Express, assuming a suitable body parser.
I know the body
objects exists and I know it has properties. Its typing today however is just any
and that suffers from all the problems outlined by the sea of discussion above: it's easy to write code that just _assumes_ that a body parameter exists or is of a given type. That is, the followoing compiles without warning: const foo: string = req.body['foo']
. However, correct code would need to use some kind of validation function that ensures that key foo
exists and is of the correct type.
It's safe to assume that body
is trusted (it is provided by the application/library) but not safe to assume that the _contents_ of body
are trusted (they are user-provided).
Conditional types are making a true top type more necessary. Some offline discussion summary.
unknown
be narrowed? If so, by what?function fn(x: unknown) {
if (Array.isArray(x)) {
// OK, or no?
const arr: any[] = x;
}
}
Yes, this is half of the entire point of the feature. Particulars:
typeof x === "string"
should narrow x
to string
Array.isArray(x)
should work as wellinstanceof
operator narrows to the instance type of the RHSif ("p" in x) {
narrows x
to { p: unknown }
Openish questions?
x > 0
imply any narrowing?x === y
? Presumably you would just use y
at that point?if (x) {
do?if (!x) {
do?x === "foo") || (x === "bar"
) to narrow x
to the union type "foo" | "bar"
? What are the practical limits of this kind of narrowing?unknown
and/or call it?No. Allowing "chained" unknown
bad because it implies you could unsafely write x.y.z
.
{ [s: string]: unknown }
mean?We currently have special behavior around { [s: string]: any }
; unknown
gets the same behavior: Anything is assignable to { [s: string]: unknown }
.
unknown
assignable to any
?Yes, necessarily - type guards are presumably written accepting any
.
unknown
assignable to {}
in non-SNC mode?Yes.
unknown
assignable to anything else?No.
unknown
?Yes.
unknown & T
?T
unknown | T
?unknown
x!
?object | string | number | boolean
?
Maybe
if (x > 0)
is not useful:"-1" > 0
-> false
"1" > 0
-> true
null > 0
-> false
null > -1
-> true
new Date(0) > 0
-> false
new Date(1) > 0
-> true
0
could exclude undefined | null
. Yay.x === y
- no opinion, seems useless though.if (x)
implies x: true | number | string | object
i.e. the truthy typeif (!x)
implies x: undefined | null | false | 0 | ""
i.e. the falsey type (what about NaN
?)x === 'foo' || x === 'bar'
to imply x: 'foo' | 'bar'
// SVG-ish...
type FillMode = 'nonzero' | 'evenodd'
type Shape = { d: PathCommand[]; fill: Fill; fillMode?; FillMode; ... };
function validateShape(input: string): Shape {
const value = JSON.parse(input);
...
if ('fillMode' in value) {
if (value.fillMode !== 'nonzero' && value.fillMode !== 'evenodd')
throw new Error('Bad Shape fillMode');
}
...
return value; // no cast needed iff checks are sufficient
}
Do we need to do anything with x === y
~I think this should be an error when one is unknown
and the other is T
, for some type-variable.~
No it should have the same behaviour as x == y, when one is {} | undefined | null
, and the other is T
. I forgot that there are different cases for when the concrete type contains {}
, instead of just being number | boolean
, for example.
In my head those cases should be the same but they aren't, hence my initial incorrect comment.
In this example:
function fn(x: unknown) {
if (Array.isArray(x)) {
// OK, or no?
const arr: any[] = x;
}
}
Does x
get narrowed to unknown[]
?
@RyanCavanaugh
I think that the meaning of unknown & T
is not T
. In contrast to T
, you can perform type tests on unknown & T
:
function f <T> (x: unknown & T): x is ComplexType & T {
// Check for ComplexType.
}
I've had a decent amount of success with the following definition:
type mixed<K extends string = never> = string | boolean | number | null | undefined
| mixedArray<K> | mixedObject<K>;
interface mixedArray<K extends string> extends Array<mixed<K>> {}
type mixedObject<K extends string> = {[key in K]?: mixed};
This allows narrowing using typeof
for the core types and Array.isArray
to get back a mixed[]
. You can then narrow the mixed[]
to tuples or specific types of arrays. You can also narrow to enums by first narrowing to string/number and then narrowing the allowed values.
Type narrowing using "a" in x
as described in #10485 does not work. Instead, you need to provide a generic type K
that is the union of all the keys in the structure you're checking against (see below). You can then "build up" your target object one property at a time by checking that the properties are not undefined.
type DeepKeyOf<T> =
T extends any[] ? never
: T extends object ? keyof T | {[K in keyof T]: DeepKeyOf<T[K]>}[keyof T]
: never;
You could extend this further and add another generic parameter to the mixed*
types to support instanceof
narrowing, but I was focused on the JSON case.
To throw my 2c in here: it would be nice to have a built-in implementation of mixed
so that library authors wouldn't use any
when they really meant unknown
. It would obviously be ideal if there were a built-in mixed
(or whatever name) that didn't need the generic hackery that this implementation uses. If #10485 were extended to work on {}
, narrowing it to {a: mixed}
, the above implementation of mixed
minus the generic params would cover the majority of use-cases.
I'm closing #23838 and posting my thoughts in here. Proper top would only be useful if:
unknown
unknown
can be assigned to nothingunknown & T = T
unknown | T = unknown
unknown extends T ? true : false = false
(in other words, follow assignability rules)This agrees exactly with Ryan's post.
No matter what fringe use cases, these kinds of types are used often inside of very complex mapped conditionals, etc, and every special case needs to be checked, so it's important that the type acts very "purely mathematical". There's some weird behavior with never
that doesn't fit the name at times, but the reason it's usable, is because it's pure. I use never
more than any other type because of that reason.
@Conaclos
function f <T> (x: T): x is ComplexType & T {
// Check for ComplexType.
}
This code is equivalent to yours. No need for unknown
, except as a hint to the programmer that there might be more to the type. That small hint is not worth making the type useless as a top type.
No matter what fringe use cases, these kinds of types are used often inside of very complex mapped conditionals, etc, and every special case needs to be checked, so it's important that the type acts very "purely mathematical".
This.
@SimonMeskens
No matter what fringe use cases, these kinds of types are used often inside of very complex mapped conditionals, etc, and every special case needs to be checked, so it's important that the type acts very "purely mathematical".
100% agreed. I couldn't tell you how many times I've had to alias type unknown = {} | null | undefined | void
just to properly and safely type something. It comes up a lot when writing highly generic code, and in probably about 50% of the TypeScript projects I've worked on have had a legitimate use for this.
Please, can we add unknown
as a proper top type, so I don't have to frequently type out that alias in so many TS projects? (It'd also open the door to better type definition writing.)
Is it safe to narrow a value of type type unknown
using in
?
const x: unknown = undefined;
if ("p" in x) {
// Statically (as proposed) x : { p: unknown }
// Dynamically: TypeError: Cannot use 'in' operator to search for 'p' in undefined
}
As x
can be anything it can be undefined
, which would cause a run-time error when applying in
.
Please laugh at me if I missed something obvious.
@jack-williams for this to be type safe I think it should be required that you ensure it's an object:
declare const x: unknown;
if (x instanceof Object) { // narrows typeof x to {}
if ('p' in x) { // narrows typeof x to { p: unknown }
const { p } = x;
}
}
in
is legal, though not particularly useful for sane code, for boolean
, number
, string
and symbol
too.
(Incidentally, it would be nice if TS had something like a primitive
type that excluded undefined
, null
and object
π€·)
That said, if (x && 'p' in x)
is natural and safe, so I'd like support for that.
Sure, I was just giving one example of a narrowing that would make the check safe. Checking that itβs a string
, number
, etc. or just truthy should all be valid ways to narrow it sufficiently.
@simonbuchan if (x && typeof x === 'object' && 'p' in x)
is safe, but if (x && 'p' in x)
isn't.
@simonbuchan in
is not legal for anything other than objects.
'p' in true
TypeError: right-hand side of 'in' should be an object, got boolean[Learn More] debugger eval code:1:1
'p' in 'str'
TypeError: cannot use 'in' operator to search for 'p' in 'str' debugger eval code:1:1
'p' in 123
TypeError: right-hand side of 'in' should be an object, got number[Learn More] debugger eval code:1:1
'p' in Symbol()
TypeError: right-hand side of 'in' should be an object, got symbol[Learn More]
My mistake! (I was sure I checked that? Maybe I got mixed up with if (x && x.p)
always being safe)
So yeah, type checking unknown
accesses would be quite handy π
Most helpful comment
I like the idea of differentiating "trust me, I know what I'm doing" from "I don't know what this is, but I still want to be safe". That distinction is helpful in localizing unsafe work.