Skip to content

unknown: less-permissive alternative to any #10715

Closed
@seanmiddleditch

Description

@seanmiddleditch

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:

  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

Activity

changed the title [-]less-permissive alternative to `any`[/-] [+]`unknown`: less-permissive alternative to `any`[/+] on Sep 5, 2016
ahejlsberg

ahejlsberg commented on Sep 6, 2016

@ahejlsberg
Member

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

dead-claudia commented on Sep 6, 2016

@dead-claudia

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

yortus commented on Sep 6, 2016

@yortus
Contributor

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

dead-claudia commented on Sep 6, 2016

@dead-claudia

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

yortus commented on Sep 6, 2016

@yortus
Contributor

@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

dead-claudia commented on Sep 6, 2016

@dead-claudia

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
#10715 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBC3YMJnttuqXk4Dq6iTh9VC7orY2ks5qnNg7gaJpZM4J1Wzb
.

RyanCavanaugh

RyanCavanaugh commented on Sep 28, 2016

@RyanCavanaugh
Member

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

saschanaz commented on Sep 28, 2016

@saschanaz
Contributor

@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

dead-claudia commented on Sep 29, 2016

@dead-claudia

@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
#10715 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBO-yD4Qdc605NubmW6yKrBXH5ZFTks5quvQUgaJpZM4J1Wzb
.

mhegazy

mhegazy commented on Sep 29, 2016

@mhegazy
Contributor

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

93 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FixedA PR has been merged for this issueIn DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @pelotom@jcready@marcind@OliverJAsh@DanielRosenwasser

        Issue actions

          `unknown`: less-permissive alternative to `any` · Issue #10715 · microsoft/TypeScript