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

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

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

@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

@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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did this type collapse to true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

@weswigham weswigham May 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@wesleyolis
Copy link

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

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

Gudahtt added a commit to MetaMask/core 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/core 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)
MajorLift pushed a commit to MetaMask/core that referenced this pull request Oct 11, 2023
* 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)
MajorLift pushed a commit to MetaMask/core that referenced this pull request Oct 11, 2023
* 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
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants