TypeScript: Some way to express a non-null / non-undefined any would be nice

While updating the various .d.ts to have | null and | undefined, I came across some typings that use any but don’t allow null or undefined.

  • Object.defineProperty:

    /**
      * Adds a property to an object, or modifies attributes of an existing property. 
      * @param o Object on which to add or modify the property. This can be a native JavaScript 
      * object (that is, a user-defined object or a built in object) or a DOM object.
      * @param p The property name.
      * @param attributes Descriptor for the property. It can be for a data property or an accessor
      *  property.
      */
    defineProperty(o: any, propertyKey: PropertyKey, attributes: PropertyDescriptor): any;
    

    o and the returned value cannot be null or undefined.

  • Object.setPrototypeOf:

    /**
      * Sets the prototype of a specified object o to  object proto or null. Returns the object o.
      * @param o The object to change its prototype.
      * @param proto The value of the new prototype or null.
      */
    setPrototypeOf(o: any, proto: any): any;
    

    o and the returned value cannot be null or undefined. proto can be null but not undefined.

  • etc.

I think there is value in being able to express both “This can be any type including null and undefined” and “This can be any type excluding null / undefined”, but there is no way to express the latter.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 34
  • Comments: 59 (23 by maintainers)

Most upvoted comments

This type already exists. It’s {}

declare function f(x: {}): void;

f(0);           // ok
f("");          // ok
f(true);        // ok
f({});          // ok
f(null);        // error
f(undefined);   // error

Maybe a new builtin type name like some or something, where some | null | undefined ≡ any

The following example, shows that string | number | boolean | symbol | object is not the same as any - null - undefined.

function f(p: string | number | boolean | symbol | object) {
    p.something // error
}
function f(p: any - null - undefined) {
    p.something // no error
}

any doesn’t mean that the value can be of any type. It means “please disable all type checking thanks”. It doesn’t make a whole lot of sense to “disable type checking except for null checks”.

To declare a variable that can be of any type you can use unknown. Sadly, you can’t do NonNullable<unknown> because it’s not a union type, so string | number | boolean | symbol | bigint | object seems to be the only way to do this right now.

Since this thread comes up in google searches. This is the best you can do to get at least some errors caught:

export type thing = { [key: string]: any };
export type anything = thing | undefined | null;

I’m ok with doing string | number | boolean | ... as a workaround but is there some way to display a custom alias like Defined or Some in error messages instead of the full definition?

I’m working with generated code which contains lots of statements about things that are defined and the error messagages are extremely hard to read because of such long definitions. See https://github.com/maasglobal/maas-schemas-ts/blob/master/src/core/booking.ts for example.

@dcodeIO

declare function assert<T>(isTrueish: T | null | undefined | 0 | "", message?: string): T;

declare const p: null | string;
const p2 = assert(p); // p2: string

I am using T | null | undefined and it seems to be working well:

export function Some<T>(_:T|null|undefined):_Some<T>|_None<T> {
    return assert_some(_) ? new _Some(_ as T) : new _None<T>();
}

See https://github.com/Threestup/monads

I’m sorry if I’m not involved in the project enough to know whether this conversation is finished, but here’s my take:

How about a new global that is an alias to string | number | boolean | symbol | object.

It will be updated to include new primitives, if/when any are added to the language (see recent addition of symbols).

interface State {
    message?: some;
}

@mmkal Seems to be related to #13195

maybe you can try use T extends null ? never : T, as follows: image

@RyanCavanaugh @cwharris

Of course they’re not the same thing.

export type thing = object | string | boolean | symbol | number;

function example1(d: thing) {
    d.someProp = 5;
// runtime/test.ts(4,7): error TS2339: Property 'someProp' does not exist on type 'thing'.
//  Property 'someProp' does not exist on type 'string'.
}

@RyanCavanaugh

For return types, it’s probably best to leave it as any. I’m not sure what use there is in saying “any but not null or undefined” since you’re able to assign it to a non-null type anyway.

null-checks and undefined-checks!

That’s one of the really really big points to using static analysis to begin with. any is flawed in that it’s meaning is “do whatever” but in many contexts what we really want to say is “it’s whatever [that’s not null/undefined]” and it’s really easy to use it like that.

I’d say this is a failure of english if nothing else. Same words but totally different meanings “it’s a thing that exists” vs “this is not checked.” This is terrible for code readability too.

This is the problem: “any = can be anything including null or undefined but will NEVER checks for it”

Consider this code:

get_db_entry(query, parseEntry);
function processEntry(err: any, entry: any) {
   entry['someKey'] = [];
   // ^ entry could very well be undefined here, typescript don't care
}

This is how we would like it to look:

get_db_entry(query, parseEntry);
function processEntry(err: any, entry: thing|undefined) {
   entry['someKey'] = [];
   // ^ Error: entry could be undefined
}

// or if we omit use just "thing" there then the error would be on get_db_entry
// because it's getting passed an incompatible function

There’s no way to declare a “thing” type either, since you can’t do recursive types. Note the [] there, that actually would result in some sort of never[] assignment error one way or another. It’s nearly impossible to declare it with out treating it as a special case each time (which is at odds to you wanting to use any there to begin with).

Just want to also echo what was said earlier. We don’t need over-engineered sophistication such as any - null - undefined. We just want a built-in thing type that’s exactly like any except it doesn’t allow null / undefined.

@Arnavion @RyanCavanaugh I think this should be reopened - there’s another simple use case for the some idea, which isn’t covered by object or {}: forcing consumers of a type or interface to handle a falsy value explicitly.

What we have to do currently:

interface State {
    message: any;
}

const thingThatUsesState = (state: State) => {
    if (state.message.text === "foo") {
         console.log("message text is foo");
    } else {
         console.log(JSON.stringify(state.message));
    }
};

in the above, state.message could be null or undefined, and we’ll get a runtime error. TypeScript doesn’t protect me against that by giving a compile-time error when I assume state.message is there. If we could do:

interface State {
    message?: some;
}

The compiler would highlight the bug in the code above and correctly force me to fix it like this:

const thingThatUsesState = (state: State) => {
    if (!state.message) {
        console.log("message is missing");
    } else if (state.message.text === "foo") {
         console.log("message text is foo");
    } else {
         console.log(JSON.stringify(state.message));
    }
};

I think the core of the issue is that people assume any == { [key: string]: any } | object | string | boolean | symbol | number | null | undefined, and that is not the case. any is given carte blanche - ignored by type checking altogether. It’s a way to enforce explicitly declaring the type of all variables (a la noExplicitAny) even if it does not have a type.

TypeScript only works with types. It ignores anything which does not have a type. This includes any. any is a very good solution to a very hard problem, even if the naming is a bit weird (it implies that it is any of the built-in types). any is a feature which could only be implemented as part of the TypeScript language.

What some people want is for the TypeScript team to spend time making a some type which is an actual type defined as any (see how this is confusing?) built-in type except any (which is not a type).

But instead of spending time compounding the any confusion by adding a some type, it looks like the TypeScript teams has decided to focus on features that can't be easy added without modifying the compiler.

Personally, I think some is a bad use of TypeScript’s development resources. You can write your own some in 10 seconds, and things will just work out how you want them to. Plus you get to tell all your friends how annoying any is, and how it’s the only type in TypeScript that isn’t a type.

type isnt = any;
type some = { [key: string]: some } | object | string | boolean | symbol | number | null | undefined;
type kinda = some | { [key: string]: any };

@mhegazy I have no problem with verbosity, especially since (a) you can define aliases and (b) few APIs actually returns all kind of types. Quite often any are just unknown object shapes.

I was pointing out that any has one big difference to the union of all types: it is a special type that bypasses all compiler checks! This is why changing a return type from any to object is a breaking change, see the examples in my previous comment.

Personally I can live with any being unclear about nulls (it’s not checked anyway) but if we want to support better documentation without introducing new type operators, I suggested previously that we start the habit of using any | null for nullables any and just any for the non-nullable ones.

Again, it won’t change compilation but it can be seen as helpful from a documentation perspective.

{} would be the equivalent type from the input side.

On the output side, it would be string | number | boolean | object (note that object is unimplemented, see #1809)