Repository navigation
unknown: less-permissive alternative to any #10715
Description
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: #10715 (comment)
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
anytype here. This is done with theJSON.parsereturn 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
Objecttype 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
Activity
[-]less-permissive alternative to `any`[/-][+]`unknown`: less-permissive alternative to `any`[/+]ahejlsberg commentedon Sep 6, 2016
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.
dead-claudia commentedon Sep 6, 2016
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.
yortus commentedon Sep 6, 2016
For anyone interested, there's a good deal of related discussion about the pros/cons of
anyand{}in #9999. The desire for a distinctunknowntype is mentioned there, but I really like the way Sean Middleditch (@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.dead-claudia commentedon Sep 6, 2016
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'sNothing), but they still have a guarded entrance (Haskell'sforall a. atype, Scala'sAny). In a sense,anyis TypeScript's bottom type, while{} | voidor{} | null | undefined(the type ofunknownin 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'sNothing), but TypeScript names it based on what it can assign to (TypeScript'sanyassigns to thing, but TypeScript's{} | voidonly assigns to{} | void).yortus commentedon Sep 6, 2016
@isiahmeadows
anyis 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, thenanyis an impossible beast.I prefer to think of
anymore like a compiler directive that can appear in a type position that just means 'escape hatch - don't type check here'. If we think ofanyin terms of it's type-theory qualities, it just leads to contradictions.anyis 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.dead-claudia commentedon Sep 6, 2016
Okay. I see now. So
neveris the bottom type. I forgot aboutanybeingthere for supporting incremental typing.
On Mon, Sep 5, 2016, 23:04 yortus notifications@github.com wrote:
RyanCavanaugh commentedon Sep 28, 2016
This could use clarification with some more examples -- it's not clear from the example what the difference between this type and
anyare. For example, what's legal to do withanythat 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.saschanaz commentedon Sep 28, 2016
Ryan Cavanaugh (@RyanCavanaugh)
If I understand correctly:
BTW, what does the proposed-but-unimplemented
objecttype do? I haven't seen or read about it.dead-claudia commentedon Sep 29, 2016
Kagami Sascha Rosylight (@saschanaz) Your understanding matches mine, too.
On Wed, Sep 28, 2016, 19:11 Kagami Sascha Rosylight <
notifications@github.com> wrote:
mhegazy commentedon Sep 29, 2016
I think the request here is for
unknownto be{ } | null | undefined, but be allowed to "evolve" as you assert/assign into it.mihailik commentedon Oct 26, 2016
Suggestion: drop
unknownkeyword, and apply "the ability to evolve" feature to{}itself.That would limit cognitive load and proliferation of 'exception' types like
never,unknown,any,voidetc. 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
unknownkeyword: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.*
saschanaz commentedon Oct 26, 2016
I think changing existing behavior is too surprising.
50 remaining items
Conaclos commentedon Mar 9, 2018
Ryan Cavanaugh (@RyanCavanaugh)
I think that the meaning of
unknown & Tis notT. In contrast toT, you can perform type tests onunknown & T:felipeochoa commentedon Apr 1, 2018
I've had a decent amount of success with the following definition:
This allows narrowing using
typeoffor the core types andArray.isArrayto get back amixed[]. You can then narrow themixed[]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 xas described in #10485 does not work. Instead, you need to provide a generic typeKthat 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.You could extend this further and add another generic parameter to the
mixed*types to supportinstanceofnarrowing, 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
mixedso that library authors wouldn't useanywhen they really meantunknown. It would obviously be ideal if there were a built-inmixed(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 ofmixedminus the generic params would cover the majority of use-cases.SimonMeskens commentedon May 3, 2018
I'm closing #23838 and posting my thoughts in here. Proper top would only be useful if:
unknownunknowncan be assigned to nothingunknown & T = Tunknown | T = unknownunknown 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
neverthat doesn't fit the name at times, but the reason it's usable, is because it's pure. I usenevermore than any other type because of that reason.Victorien Elvinger (@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.pelotom commentedon May 3, 2018
This.
dead-claudia commentedon May 3, 2018
Simon Meskens (@SimonMeskens)
100% agreed. I couldn't tell you how many times I've had to alias
type unknown = {} | null | undefined | voidjust 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
unknownas 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.)jack-williams commentedon May 4, 2018
Is it safe to narrow a value of type type
unknownusingin?As
xcan be anything it can beundefined, which would cause a run-time error when applyingin.Please laugh at me if I missed something obvious.
pelotom commentedon May 4, 2018
Jack Williams (@jack-williams) for this to be type safe I think it should be required that you ensure it's an object:
simonbuchan commentedon May 4, 2018
inis legal, though not particularly useful for sane code, forboolean,number,stringandsymboltoo.(Incidentally, it would be nice if TS had something like a
primitivetype that excludedundefined,nullandobject🤷)That said,
if (x && 'p' in x)is natural and safe, so I'd like support for that.pelotom commentedon May 4, 2018
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.jcready commentedon May 4, 2018
Simon Buchan (@simonbuchan)
if (x && typeof x === 'object' && 'p' in x)is safe, butif (x && 'p' in x)isn't.felixfbecker commentedon May 4, 2018
Simon Buchan (@simonbuchan)
inis not legal for anything other than objects.simonbuchan commentedon May 5, 2018
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
unknownaccesses would be quite handy 😂