Skip to content

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.


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

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

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)
@echappen echappen mentioned this pull request Apr 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
10 participants