Skip to content
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
Merged

New 'unknown' top type #24439

merged 25 commits into from May 30, 2018

Conversation

@ahejlsberg
Copy link
Member

@ahejlsberg 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 New 'unknown New 'unknown' top type May 27, 2018
@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented May 29, 2018

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

@weswigham
Copy link
Member

@weswigham 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
Copy link
Member Author

@ahejlsberg 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
Copy link
Member Author

@ahejlsberg 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
Author 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
Author 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
Author 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
Author 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)

@MadaraUchiha
Copy link

@MadaraUchiha 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
Copy link
Contributor

@mhegazy 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;
}
@wesleyolis
Copy link

@wesleyolis 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
Copy link

@jean-pasqualini jean-pasqualini commented Jul 11, 2018

💩

@wesleyolis
Copy link

@wesleyolis 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
Copy link

@wesleyolis 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
Copy link

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

@darh darh mentioned this pull request Feb 6, 2020
4 of 4 tasks complete
stuft2 added a commit to stuft2/byu-browser-oauth that referenced this pull request Aug 11, 2020
The main reason for this update is that the token function returns the token object and not a string.
Fixes:
- use unknown instead of any (microsoft/TypeScript#24439)
- token object is defined in README
- user object is defined in README
Gudahtt added a commit to MetaMask/controllers that referenced this pull request Feb 25, 2021
The BaseController state now uses `unknown` rather than `any` as the
type for state properties. `unknown` is more type-safe than `any` in
cases like this where we don't know what type to expect. See here for
details [1].

This was suggested by @rekmarks during review of #362 [2].

[1]: microsoft/TypeScript#24439
[2]: #362 (comment)
Gudahtt added a commit to MetaMask/controllers that referenced this pull request Feb 25, 2021
* Use `unknown` rather than `any` for BaseController state

The BaseController state now uses `unknown` rather than `any` as the
type for state properties. `unknown` is more type-safe than `any` in
cases like this where we don't know what type to expect. See here for
details [1].

This was suggested by @rekmarks during review of #362 [2].

[1]: microsoft/TypeScript#24439
[2]: #362 (comment)

* Use type alias for controller state rather than interface

The mock controller state in the base controller tests now uses a type
alias for the controller state rather than an interface. This was
required to get around an incompatibility between
`Record<string, unknown>` and interfaces[1].

The `@typescript-eslint/consistent-type-definitions` ESLint rule has
been disabled, as this problem will be encountered fairly frequently.

[1]: microsoft/TypeScript#15300 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

10 participants