New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New 'unknown' top type #24439

Merged
merged 25 commits into from May 30, 2018

Conversation

Projects
None yet
10 participants
@ahejlsberg
Member

ahejlsberg commented May 27, 2018

This PR adds a new top type unknown which is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn't assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.

Note that this PR is technically a breaking change since unknown becomes a reserved type name.

// In an intersection everything absorbs unknown

type T00 = unknown & null;  // null
type T01 = unknown & undefined;  // undefined
type T02 = unknown & null & undefined;  // null & undefined (which becomes never in union)
type T03 = unknown & string;  // string
type T04 = unknown & string[];  // string[]
type T05 = unknown & unknown;  // unknown
type T06 = unknown & any;  // any

// In a union an unknown absorbs everything

type T10 = unknown | null;  // unknown
type T11 = unknown | undefined;  // unknown
type T12 = unknown | null | undefined;  // unknown
type T13 = unknown | string;  // unknown
type T14 = unknown | string[];  // unknown
type T15 = unknown | unknown;  // unknown
type T16 = unknown | any;  // any

// Type variable and unknown in union and intersection

type T20<T> = T & {};  // T & {}
type T21<T> = T | {};  // T | {}
type T22<T> = T & unknown;  // T
type T23<T> = T | unknown;  // unknown

// unknown in conditional types

type T30<T> = unknown extends T ? true : false;  // Deferred
type T31<T> = T extends unknown ? true : false;  // Deferred (so it distributes)
type T32<T> = never extends T ? true : false;  // true
type T33<T> = T extends never ? true : false;  // Deferred

// keyof unknown

type T40 = keyof any;  // string | number | symbol
type T41 = keyof unknown;  // never

// Only equality operators are allowed with unknown

function f10(x: unknown) {
    x == 5;
    x !== 10;
    x >= 0;  // Error
    x + 1;  // Error
    x * 2;  // Error
    -x;  // Error
    +x;  // Error
}

// No property accesses, element accesses, or function calls

function f11(x: unknown) {
    x.foo;  // Error
    x[5];  // Error
    x();  // Error
    new x();  // Error
}

// typeof, instanceof, and user defined type predicates

declare function isFunction(x: unknown): x is Function;

function f20(x: unknown) {
    if (typeof x === "string" || typeof x === "number") {
        x;  // string | number
    }
    if (x instanceof Error) {
        x;  // Error
    }
    if (isFunction(x)) {
        x;  // Function
    }
}

// Homomorphic mapped type over unknown

type T50<T> = { [P in keyof T]: number };
type T51 = T50<any>;  // { [x: string]: number }
type T52 = T50<unknown>;  // {}

// Anything is assignable to unknown

function f21<T>(pAny: any, pNever: never, pT: T) {
    let x: unknown;
    x = 123;
    x = "hello";
    x = [1, 2, 3];
    x = new Error();
    x = x;
    x = pAny;
    x = pNever;
    x = pT;
}

// unknown assignable only to itself and any

function f22(x: unknown) {
    let v1: any = x;
    let v2: unknown = x;
    let v3: object = x;  // Error
    let v4: string = x;  // Error
    let v5: string[] = x;  // Error
    let v6: {} = x;  // Error
    let v7: {} | null | undefined = x;  // Error
}

// Type parameter 'T extends unknown' not related to object

function f23<T extends unknown>(x: T) {
    let y: object = x;  // Error
}

// Anything but primitive assignable to { [x: string]: unknown }

function f24(x: { [x: string]: unknown }) {
    x = {};
    x = { a: 5 };
    x = [1, 2, 3];
    x = 123;  // Error
}

// Locals of type unknown always considered initialized

function f25() {
    let x: unknown;
    let y = x;
}

// Spread of unknown causes result to be unknown

function f26(x: {}, y: unknown, z: any) {
    let o1 = { a: 42, ...x };  // { a: number }
    let o2 = { a: 42, ...x, ...y };  // unknown
    let o3 = { a: 42, ...x, ...y, ...z };  // any
}

// Functions with unknown return type don't need return expressions

function f27(): unknown {
}

// Rest type cannot be created from unknown

function f28(x: unknown) {
    let { ...a } = x;  // Error
}

// Class properties of type unknown don't need definite assignment

class C1 {
    a: string;  // Error
    b: unknown;
    c: any;
}

Fixes #10715.

@ahejlsberg ahejlsberg changed the title from New 'unknown to New 'unknown' top type May 27, 2018

@RyanCavanaugh

This comment has been minimized.

Member

RyanCavanaugh commented May 29, 2018

Where'd we end up with mapped conditional types w.r.t unknown ?

@weswigham

This comment has been minimized.

Member

weswigham commented May 29, 2018

type T41 = keyof unknown; // string | number | symbol

Does that make sense (is this derived from something else)? If unknown is effectively the infinite union - a union of all possible types would have no common keys among all of them, leading me to believe the correct result is never (the given types seem correct for never, this being a place where any is more never-like than top-like).

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented May 29, 2018

@weswigham No, that probably doesn't make sense. We should be similar to keyof {} and keyof object here, both of which are never.

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented May 29, 2018

@RyanCavanaugh Do you mean the behavior of unknown in a distributive conditional type? It's an atomic type so it doesn't distribute. This is unlike {} | null | undefined which distributes and therefore is very hard to test for.

@@ -19624,7 +19644,7 @@ namespace ts {
}
// Functions with with an explicitly specified 'void' or 'any' return type don't need any return expressions.
if (returnType && maybeTypeOfKind(returnType, TypeFlags.Any | TypeFlags.Void)) {
if (returnType && maybeTypeOfKind(returnType, TypeFlags.AnyOrUnknown | TypeFlags.Void)) {

This comment has been minimized.

@weswigham

weswigham May 29, 2018

Member

I think a function returning an explicit unknown should probably have return values? Otherwise you should've written void or undefined, right?

This comment has been minimized.

@ahejlsberg

ahejlsberg May 29, 2018

Member

Not sure about that. undefined is in the domain of unknown (just like it is in the domain of any) and that's really what should guide us here.

This comment has been minimized.

@weswigham

weswigham May 29, 2018

Member

Hm. Fair. Do we handle unions with undefined in accordance with that, then?

This comment has been minimized.

@ahejlsberg

ahejlsberg May 30, 2018

Member

We do, but with a few wrinkles. If the return type annotation includes void, any, or unknown we don't require any return statements. Otherwise, if the return type includes undefined we require at least one return statement somewhere, but don't require return statements to have expressions and allow the end point of the function to be reachable. Otherwise, we require a return statement with an expression at every exit point.

@@ -403,7 +403,7 @@ interface B1<T> extends A1<T> {
>T : T
boom: T extends any ? true : true
>boom : T extends any ? true : true

This comment has been minimized.

@mhegazy

mhegazy May 29, 2018

Contributor

why did this type collapse to true?

This comment has been minimized.

@ahejlsberg

ahejlsberg May 29, 2018

Member

I changed this to eagerly resolve to true when the extends type is any or unknown (since it always will be).

This comment has been minimized.

@weswigham

weswigham May 29, 2018

Member

Ah, but what about T extends any ? { x: T } : never - that needs to distribute.

This comment has been minimized.

@ahejlsberg

ahejlsberg May 30, 2018

Member

Hmm, yes, I suppose we can only optimize when the conditional type isn't distributive.

This comment has been minimized.

@weswigham

weswigham May 30, 2018

Member

There's a better check (than just distributivity) that I have an outstanding PR for: instantiate the check type (and infer types) in the true/false types with wildcards. If the instantiation isn't the input type, you can't simplify. If it is, you can. (Since the type isn't affected by instantiation)

@ahejlsberg ahejlsberg merged commit a8a31ed into master May 30, 2018

5 checks passed

VSTS: node10 2984 succeeded
Details
VSTS: node6 2982 succeeded
Details
VSTS: node8 2983 succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
license/cla All CLA requirements met.
Details

@ahejlsberg ahejlsberg deleted the unknownType branch May 30, 2018

@Retsam

This comment has been minimized.

Retsam commented May 30, 2018

Awesome, I'm really glad to see this go in. Thanks @ahejlsberg .

Something discussed in #10715 that I don't see mentioned one way or another, does the in operator narrow unknown? (e.g."key" in x would narrow x to {key: unknown})

@c69

This comment has been minimized.

c69 commented Jun 3, 2018

type T16 = unknown | any; // any

isn't it supposed to be unknown, because union with unknown absorbs everything into unknown ?

@Yogu

This comment has been minimized.

Contributor

Yogu commented Jun 3, 2018

let v7: {} | null | undefined = x;  // Error

As I see, the unknown type is kind of an alias to {}|null|undefined, which is the current way of specifying an unkown type. To improve backwards compatibility, it would be nice if {}|null|undefined was assignable to unknown and vice-versa. Have you considered this and dismissed it because it is too complex?

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 3, 2018

@c69 any and unknown are both assignable to each other, so either would be a valid choice. We give the nod to any here because any has additional capabilities and it was always the case that any in a union type absorbs everything.

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 3, 2018

@Yogu {} | null | undefined is already assignable to unknown (because anything is). The reverse is currently not true, and I'm thinking we want to keep it that way so that there is never any doubt as to which is the true top type.

@ahejlsberg

This comment has been minimized.

Member

ahejlsberg commented Jun 3, 2018

@Retsam You're welcome!

Something discussed in #10715 that I don't see mentioned one way or another, does the in operator narrow unknown? (e.g."key" in x would narrow x to {key: unknown})

That isn't part of this PR and I think it is basically an orthogonal feature request. I suggest opening a new issue for it.

@MadaraUchiha

This comment has been minimized.

MadaraUchiha commented Jun 23, 2018

Will there be any core typings changes? The original issue #10715 speaks of making JSON.parse() return unknown, is something like that planned?

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Jun 25, 2018

Will there be any core typings changes? The original issue #10715 speaks of making JSON.parse() return unknown, is something like that planned?

no. this would be a breaking change that we are not planning on doing.

you can define locally parse and it should take precedence over the one in the library:

interface JSON {
    parse(text: string, reviver?: (key: any, value: any) => any): unknown;
}

@ikokostya ikokostya referenced this pull request Jun 27, 2018

Merged

[js-yaml] Revert return type of loader functions to any #26883

7 of 9 tasks complete
@wesleyolis

This comment has been minimized.

wesleyolis commented Jul 5, 2018

In ways this could be quite similar to the proposal of void being allowed to morph into into the first type that that it is operated on.... Mabye need to compare differences pros and cons.
#24852

@jean-pasqualini

This comment has been minimized.

jean-pasqualini commented Jul 11, 2018

💩

@wesleyolis

This comment has been minimized.

wesleyolis commented Jul 12, 2018

Hi,

Just a couple of other use cases for unknown with infer typings, which just found myself wanting to be able to do, which you could use also use as test cases and proof for this implementation

type CallBackMethod =  ((callback: (err: any, result:  any) => void) => void)
type CallBackMethodExtract<T> =  T extends ((callback: (err: any, result: infer R) => void) => void) ? R : undefined
type ExtractCallbackResultCallBack<T> = T extends CallBackMethod ? CallBackMethodExtract<T> : undefined

type ExtractCallbackResult<T> = T extends ((callback: (err: any, result: infer T) => void) => void) ? T: undefined


const testsCallBack = {
	test : function (callback : (err : any, result : string) => void)
}

function Prom<R extends any, M extends keyof O = unknown, O extends {} = unknown>(object : O, method : M) : () => Bluebird<R extends undefined ? ExtractCallbackResult<O[M]> : R>
function Prom<T>(propA: (callback: (err: any, result: T) => void) => void) : () => Bluebird<T>;


function Prom(propA : any, propB : any = undefined) : any
{
	if (propB === undefined)
		return Bluebird.promisify(propA)();
	else
		return Bluebird.promisify(propA[propB], propB)();
}

const sftpWrapper = Prom<ssh2.SFTPWrapper>(testsCallBack, 'test');// required to overide a bad implementation of the typings.

const sftpWrapper = Prom<ssh2.SFTPWrapper>(testsCallBack, 'tet');// Compiler Error
const sftpWrapper = Prom(ssh2, 'fastGet');// returns a good implementation of the types libary

It could also be argued, that type inference, should override defaults value for generics and that this is a bug type inference/generics bug.

@wesleyolis

This comment has been minimized.

wesleyolis commented Jul 12, 2018

The other case to mention is the following when dealing with generics, which is a problem were generic parameters that are dangling, at the start of function are given the {} which means, one is not able to detect a default infer type from thin air, to do something with it, because extends {} is basically that infer type for the dangling Generic. It should rather infer unknown as the type when coming form thin air.

This case is not detectable, as T1 is infer from thin air.

function func<T1, T2, T3>(param1 : boolean, param2 : string) : T1 extends {} ? never : T1
{
}
const results = func(true, "something") // this is always never for any object, now.

It would be better if the following was the case

function func<T1, T2, T3>(param1 : boolean, param2 : string) : T1 extends unkown ? never : T1
{
}
const results = func(true, "something") // this is never, only when T1 is not specified other wise would be the object type.
type MyObject = {}
const results func<MyObject>(true, "something");// this results in {}, which what we want.

This case is not detectable, as T1 is infer from thin air, say this should not be the function signature to use, by explicitly returning unknown, versus indirectly as pass thought type or inference, extract from a generic. As it would allow one to differentiate between function signatures, that are similar.

function fun<K extends keyof O, O extends {}>(param1 : O, param2 : K) : ExtractionOfType<O[K]>
function func<T1, T2, T3>(param1 : boolean, param2 : string) : T1 extends {} ? unknown : T1
function(param1 : any, param2 : any)
{
}
const results = func({key:...}, "key") // were the type is of the extracted type function from O[K],
because other function signature explicitly returned unknown.
@wesleyolis

This comment has been minimized.

wesleyolis commented Jul 12, 2018

Probably also a good think to keep in mind with all of this, return type statment...

Typically I envisage this to be order thing, were one would typically choose the least constraining function call when the type system was to intersect the function type signature.
This is typically not what we want when we wanting to return different return type signature based on input signature, we wan the return type signature to be choose from the function call signature, that has the stricts constrains that match.

What are you thoughts regarding, this belongs with the other post, it fit with the partial infer type from air of that post.

const testsCallBack = {
	test : function (callback : (err : any, result : string) => void)
}

function Prom<R>(object : {}, method : string) : () => Bluebird<R>
function Prom<O extends {}>(object : O, method : keyof O) : Bluebird<Extract<O>>

const sftpWrapper = Prom(testsCallBack, 'test'); // return type BlueBird<{}>

I think the part of concern would be the return type and finding the most restrictive match to return the return type to use, see some work has been done on looks like be on in the next release regarding remain non explicit types, will be inferred.

#25603

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