microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.96k stars 12.48k forks source link

`unknown`: less-permissive alternative to `any` #10715

Closed seanmiddleditch closed 6 years ago

seanmiddleditch commented 8 years ago

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
ahejlsberg commented 8 years ago

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.

dead-claudia commented 8 years ago

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 commented 8 years ago

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.

dead-claudia commented 8 years ago

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).

yortus commented 8 years ago

@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.

dead-claudia commented 8 years ago

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 notifications@github.com 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 .

RyanCavanaugh commented 8 years ago

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.

saschanaz commented 8 years ago

@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.

dead-claudia commented 8 years ago

@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 < notifications@github.com> 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 .

mhegazy commented 8 years ago

I think the request here is for unknown to be { } | null | undefined, but be allowed to "evolve" as you assert/assign into it.

mihailik commented 8 years ago

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:

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!");

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:

if (bla.price && bla.articleId && bla.completed)
  acknowledgeOrder(bla);

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.***

saschanaz commented 8 years ago

I think changing existing behavior is too surprising.

let o1 = {};
o1.foo // okay

let o2 = { bar: true };
o1.foo // suddenly not okay :/
mihailik commented 8 years ago

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.

mihailik commented 8 years ago

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
dead-claudia commented 8 years ago

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:

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 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 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!");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 .

mihailik commented 8 years ago

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.

dead-claudia commented 7 years ago

A top type like this unknown would be immensely useful for type safety reasons. It gets old typing {} | void each time.

pelotom commented 6 years ago

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.

marcind commented 6 years ago

Have not seen it mentioned here, but this proposal looks quite similar to the mixed type in Flow.

dead-claudia commented 6 years ago

@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.

pelotom commented 6 years ago

@isiahmeadows

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.

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]
RyanCavanaugh commented 6 years ago

Long and short of the notes from #20284: What (if anything) should make unknown different from a type alias for {} | null | undefined ?

saschanaz commented 6 years ago

@RyanCavanaugh

var x: unknown;
x.x // should still be unknown, but error on `{} | null | undefined`
pelotom commented 6 years ago

@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.

saschanaz commented 6 years ago

What? We already have that, it's called any.

Then the x.x becomes any instead of unknown which has a different behavior.

pelotom commented 6 years ago

Then the x.x becomes any instead of unknown 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.

RyanCavanaugh commented 6 years ago

Is x.x(3) valid (returns unknown) ?

saschanaz commented 6 years ago

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?)

pseudo_interface unknown extends null|undefined|Object {
  [key: any]: unknown // this[?] is unknown
  [any]: unknown // this.? is unknown
  [key: any](): ()=>unknown // this.?() returns unknown
}
marcind commented 6 years ago

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

pelotom commented 6 years ago

Is x.x(3) valid (returns unknown) ?

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.

pelotom commented 6 years ago

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.

pelotom commented 6 years ago
if (unknownFoo.bar) unknownFoo.bar // ok

10485 would allow this to work:

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?

saschanaz commented 6 years ago

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.

pelotom commented 6 years ago
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.

yortus commented 6 years ago

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.

saschanaz commented 6 years ago

Why is the first any better than the second though?

An author can be sure that any unsafe object is not being passed around.

yortus commented 6 years ago

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 types

mixed will accept any type of value. Strings, numbers, objects, functions– anything will work. [....] When you try to use a value of a mixed type you must first figure out what the actual type is [with a type guard] or you’ll end up with an error.

any types

If you want a way to opt-out of using the type checker, any is the way to do it. Using any 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.

pelotom commented 6 years ago

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!

marcind commented 6 years ago

@pelotom what would be a true top type?

pelotom commented 6 years ago

@marcind {} | undefined | null

saschanaz commented 6 years ago

Using any is completely unsafe, and should be avoided whenever possible.

I like this 👍

Sounds better to file a new issue for mixed?

dead-claudia commented 6 years ago

@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.

saschanaz commented 6 years ago

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();

pelotom commented 6 years ago

If it's time to start bikeshedding names...

marcind commented 6 years ago

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?

marcind commented 6 years ago

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.

marcind commented 6 years ago

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.

pelotom commented 6 years ago

@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 from Object (which is, of course, different from object).

marcind commented 6 years ago

Thanks @pelotom. Marius Schulz's blog is super helpful and I should've know to turn there for answers.

dead-claudia commented 6 years ago

@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.