-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
unknown
: less-permissive alternative to any
#10715
Comments
any
unknown
: less-permissive alternative to any
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. |
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. |
For anyone interested, there's a good deal of related discussion about the pros/cons of
Being able to express a clear distinction between trusted ( |
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 I think the biggest source of confusion is that most languages name their top type based on what extends it (everything extends Scala's |
@isiahmeadows I prefer to think of |
Okay. I see now. So On Mon, Sep 5, 2016, 23:04 yortus notifications@github.com wrote:
|
This could use clarification with some more examples -- it's not clear from the example what the difference between this type and With |
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 |
@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 <
|
I think the request here is for |
Suggestion: drop That would limit cognitive load and proliferation of 'exception' types like EvolvabilityThe currently existing Evolvable means you can probe its properties liberally without casting. Probing means accessing property in special known positions: var bla: {};
if (bla.price) console.log("It's priced!"); // GOOD, we **probed** price
console.log(bla.price); // BAD, we're **using** price, which isn't safe
if (bla.price) console.log(bla.price); // GOOD, we **probed**, then we can use Probing works very similar to type assertions, and in a conventional JS coding style too. After a property is probed, the type of the expression changes to I suggest these three supported ways of probing: // non-null probing, asserts {} & { property: any; }
if (bla.price) console.log("priced!");
// property probing, asserts {} & { property: any | null | undefined }
if ('price' in bla) console.log("priced!");undefined; }
// typed probing, asserts {} & { property: type; }
if (typeof bla.price==='number') console.log("priced!");}
// custom assert probing, asserts {} & { property: type; }
if (isFinite(bla.price)) console.log("priced!"); IntersectionIt's crucial to allow "evolability" to more than just one selected type, but intersections too. Consider multi-property asserts that naturally come out of it: if (bla.price && bla.articleId && bla.completed)
acknowledgeOrder(bla); No unknowns pleaseLastly I want to highlight the real danger of unknown Those two are way too similar, and be confused in all sorts of situations. Mere typos would be a big problem in itself. But factor in genuine long-term misconceptions this similarity would inevitably breed. Picking another, less similar keyword might help, but going straight for an existing syntax is much better. The point of So this extra sugar added on UPDATE: replaced unions with intersections up across the text, my mistake using wrong one.* |
I think changing existing behavior is too surprising. let o1 = {};
o1.foo // okay
let o2 = { bar: true };
o1.foo // suddenly not okay :/ |
No, the first is not OK either — you're not probing there (for non-null probing it would require a boolean-bound position to qualify). With the probing definitions outlined above, compiler still errors on genuine errors, but it would handle probing/evolving neatly without excessive verbosity. |
Also note that {} naturally fits with Not necessary the case with var x: unknown;
x = null // is it an error? you would struggle to guess, i.e. it impedes readability
var x: {};
x = null; // here the rules are well-known |
I really don't like this. It removes a level of type safety in the On Wed, Oct 26, 2016, 08:52 mihailik notifications@github.com wrote:
|
I think you're missing a point @isiahmeadows — "evolvability" is only enabled for Most normal types won't have that feature: If you have an example with boolean flags, let's see how it works. |
A top type like this |
Maybe
// SVG-ish...
type FillMode = 'nonzero' | 'evenodd'
type Shape = { d: PathCommand[]; fill: Fill; fillMode?; FillMode; ... };
function validateShape(input: string): Shape {
const value = JSON.parse(input);
...
if ('fillMode' in value) {
if (value.fillMode !== 'nonzero' && value.fillMode !== 'evenodd')
throw new Error('Bad Shape fillMode');
}
...
return value; // no cast needed iff checks are sufficient
} |
No it should have the same behaviour as x == y, when one is In my head those cases should be the same but they aren't, hence my initial incorrect comment. In this example: function fn(x: unknown) {
if (Array.isArray(x)) {
// OK, or no?
const arr: any[] = x;
}
} Does |
@RyanCavanaugh function f <T> (x: unknown & T): x is ComplexType & T {
// Check for ComplexType.
} |
I've had a decent amount of success with the following definition: type mixed<K extends string = never> = string | boolean | number | null | undefined
| mixedArray<K> | mixedObject<K>;
interface mixedArray<K extends string> extends Array<mixed<K>> {}
type mixedObject<K extends string> = {[key in K]?: mixed}; This allows narrowing using Type narrowing using type DeepKeyOf<T> =
T extends any[] ? never
: T extends object ? keyof T | {[K in keyof T]: DeepKeyOf<T[K]>}[keyof T]
: never; You could extend this further and add another generic parameter to the To throw my 2c in here: it would be nice to have a built-in implementation of |
I'm closing #23838 and posting my thoughts in here. Proper top would only be useful if:
This agrees exactly with Ryan's post. No matter what fringe use cases, these kinds of types are used often inside of very complex mapped conditionals, etc, and every special case needs to be checked, so it's important that the type acts very "purely mathematical". There's some weird behavior with function f <T> (x: T): x is ComplexType & T {
// Check for ComplexType.
} This code is equivalent to yours. No need for |
This. |
100% agreed. I couldn't tell you how many times I've had to alias Please, can we add |
Is it safe to narrow a value of type type const x: unknown = undefined;
if ("p" in x) {
// Statically (as proposed) x : { p: unknown }
// Dynamically: TypeError: Cannot use 'in' operator to search for 'p' in undefined
} As Please laugh at me if I missed something obvious. |
@jack-williams for this to be type safe I think it should be required that you ensure it's an object: declare const x: unknown;
if (x instanceof Object) { // narrows typeof x to {}
if ('p' in x) { // narrows typeof x to { p: unknown }
const { p } = x;
}
} |
|
Sure, I was just giving one example of a narrowing that would make the check safe. Checking that it’s a |
@simonbuchan |
@simonbuchan
|
My mistake! (I was sure I checked that? Maybe I got mixed up with |
The
any
type is more permissive than is desired in many circumstances. The problem is thatany
implicitly conforms to all possible interfaces; since no object actually conforms to every possible interface,any
is implicitly type-unsafe. Usingany
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 ofunknown
is that it does not conform to any interface but refines to any interface. At the simplest, type casting can be used to convertunknown
to any interface. All properties/indices onunknown
are implicitly treated asunknown
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 toany
which is good for trusted data, e.g. data which could match an interface and we're comfortable assuming that to be true. Whereany
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.,
Very roughly,
unknown
is equivalent to the pseudo-interface: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:
any
type here. This is done with theJSON.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.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:
The version using
Object
is cumbersome:With the proposed
unknown
type;The text was updated successfully, but these errors were encountered: