TypeScript: `unknown`: less-permissive alternative to `any`
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:
- Use the
any
type here. This is done with theJSON.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. - 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
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 40
- Comments: 74 (26 by maintainers)
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.
Conditional types are making a true top type more necessary. Some offline discussion summary.
Can
unknown
be narrowed? If so, by what?Yes, this is half of the entire point of the feature. Particulars:
typeof x === "string"
should narrowx
tostring
Array.isArray(x)
should work as wellinstanceof
operator narrows to the instance type of the RHSif ("p" in x) {
narrowsx
to{ p: unknown }
Openish questions?
x > 0
imply any narrowing?x === y
? Presumably you would just usey
at that point?if (x) {
do?if (!x) {
do?x === "foo") || (x === "bar"
) to narrowx
to the union type"foo" | "bar"
? What are the practical limits of this kind of narrowing?Can you access properties of an
unknown
and/or call it?No. Allowing “chained”
unknown
bad because it implies you could unsafely writex.y.z
.What does
{ [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 }
.Is
unknown
assignable toany
?Yes, necessarily - type guards are presumably written accepting
any
.Is
unknown
assignable to{}
in non-SNC mode?Yes.
Is
unknown
assignable to anything else?No.
Is everything assignable to
unknown
?Yes.
What is the meaning of
unknown & T
?T
What is the meaning of
unknown | T
?unknown
What’s the type of
x!
?object | string | number | boolean
?For anyone interested, there’s a good deal of related discussion about the pros/cons of
any
and{}
in #9999. The desire for a distinctunknown
type is mentioned there, but I really like the way @seanmiddleditch has presented it here. I think this captures it brilliantly: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.Have not seen it mentioned here, but this proposal looks quite similar to the
mixed
type in Flow.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 usenever
more than any other type because of that reason.@Conaclos
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.I think a huge improvement would already to make it possible to narrow
any
with type guards. @ahejlsberg this for example does not work:json.errors
is stillany
after theArray.isArray()
check, instead ofany[]
. So I could mistype.length
, I don’t get autocompletion forjson.errors.map()
etc.I think the request here is for
unknown
to be{ } | null | undefined
, but be allowed to “evolve” as you assert/assign into it.Beyond simply adding an alias for
unknown
, it’d be really nice if core APIs were modified to returnunknown
where appropriate: e.g.JSON.parse
returningunknown
andArray.isArray(x)
would be a type guard forx is unknown[]
. (Rather thanx 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 asany
, 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.
I just followed @marcind 's link above, and I think it’s a nice and simple write-up of the case for
unknown
(calledmixed
there), and the different case forany
, which many mistake for the top type. It also mentions refinements as simply being what typescript calls type guards.mixed types
any
typesBy 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 likeany
.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 whatany
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 useany
, the difference being that it doesn’t disable type-checking.I’m not sure I follow your example or reasoning.
An example use of
{[key: any]: unknown}
would be theRequest.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 justany
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 keyfoo
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 ofbody
are trusted (they are user-provided).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.Evolvability
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:
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:
Intersection
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:
No unknowns please
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.*
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:
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.
This.
@neoncom
This tslint rule might help: https://palantir.github.io/tslint/rules/no-unsafe-any/
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. 😃@saschanaz Flow’s
mixed
is basically this proposal’sunknown
(or what it evolved to be in the comments, at least). The only difference is the name.I think the idea from OP is this:
Summary: Play with your unknown toy in your room, not in the entire house.
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 notifications@github.com wrote:
@SaschaNaz Your understanding matches mine, too.
On Wed, Sep 28, 2016, 19:11 Kagami Sascha Rosylight < notifications@github.com> wrote:
@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, thenany
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 ofany
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.~I think this should be an error when one is
unknown
and the other isT
, for some type-variable.~No it should have the same behaviour as x == y, when one is
{} | undefined | null
, and the other isT
. I forgot that there are different cases for when the concrete type contains{}
, instead of just beingnumber | boolean
, for example.In my head those cases should be the same but they aren’t, hence my initial incorrect comment.
In this example:
Does
x
get narrowed tounknown[]
?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.
I like this 👍
Sounds better to file a new issue for
mixed
?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
andvoid
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!An author can be sure that any unsafe object is not being passed around.
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
.A top type like this
unknown
would be immensely useful for type safety reasons. It gets old typing{} | void
each time.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
: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 withany
that would be illegal to do withunknown
?With
{}
,any
,{} | null | undefined
, the proposed but unimplementedobject
, andnever
, 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.