microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.11k stars 12.5k forks source link

Suggestion: Allow getters to have predicate return types #43368

Open rsimoes opened 3 years ago

rsimoes commented 3 years ago

Suggestion

πŸ” Search Terms

getter, predicate, type guard

βœ… Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

This has been requested before (e.g., #6994) but declined because at the time getters were merely an aspect of implementation. Since that is no longer the case, would there be any interest in revisiting the suggestion?

πŸ“ƒ Motivating Example

class Person {
    name?: string;
}

class Thing {
    constructor(public owner: Person | null = null) {}

    get isOwned(): this is Owned<this> {
      return !!this.owner;
    }
}

type Owned<T extends Thing> = T & { owner: NonNullable<T['owner']> };

πŸ’» Use Cases

I have a use case much like the above. Currently type narrowing by checking thing.owner isn't honored when thing is used as a function argument:

function doSomethingWithOwnedThing(ownedThing: Owned<Thing>) {}

declare const thing: Thing;

if (thing.owner) {
    thing.owner.name; // ok
    doSomethingWithOwnedThing(thing); // not ok (thing.owner is possibly `null`)
}

With the above, I can create a normal function or method that returns a type predicate or otherwise simply assert that thing is an Owned<Thing>. Given that user-defined type guards are encapsulated type assertions, and getters are shiny and neat, a boolean getter that doubles as a type guard would be nicest.

RyanCavanaugh commented 3 years ago

I'm not sure if/how this could be implemented without significant perf penalty -- property access doesn't currently create a type narrowing branch on the parent object, but we'd have to everywhere for this to work. Worth hearing feedback on, though. Never mind (see below)

ilogico commented 3 years ago

Some property accesses already contribute to type narrowing:

interface A { isOwned: false }
interface B { isOwned: true, owner: object }
type C = A | B;

declare const x: C;

if (x.isOwned) {
    x.owner;
}

But this only works when the type is a union. So maybe a type with such a property could be seen as a discriminating union.

egasimus commented 1 year ago

Use case:

enum Mode {
  Production = 'prod',
  Development = 'dev',
  // other modes - Staging, Testing, etc...
}

class Environment_v1 {
  constructor (readonly mode: Mode) {}
  get isProduction () /* I would like to add the type guard here. */ {
    return this.mode === Mode.Production
  }
  get isDevelopment () /* I would like to add the type guard here. */ {
    return this.mode === Mode.Development
  }
  // etc...
}

assert(new Environment_v1(Mode.Production).isProduction  === true)
assert(new Environment_v1(Mode.Production).isDevelopment === false)

I would gladly change the getter isProduction to instead be a method isProduction() so that code that consumes this API would benefit from the type discrimination. However... all existing uses of env.isProduction and env.isDevelopment would now return truthy.

This would, of course, be a breaking change to the API. Nothing scary, just a major version bump... except that, should downstream forget to replace even one usage of the getter with a method call when upgrading to the new major version, all hell would break loose.

class Environment_v2 {
  constructor (readonly mode: Mode) {}
  /* not a getter anymore */ isProduction (): this is ProductionEnvironment {
    return this.mode === Mode.Production
  }
  /* not a getter anymore */ isDevelopment (): this is DevelopmentEnvironment {
    return this.mode === Mode.Development
  }
}

assert(new Environment_v2(Mode.Production).isProduction()  === true)  // correct
assert(new Environment_v2(Mode.Production).isProduction    === true)  // accidentally correct
assert(new Environment_v2(Mode.Production).isDevelopment() === false) // correct
assert(new Environment_v2(Mode.Production).isDevelopment   === true)  // BOOM!
jetkai commented 1 year ago

bump

patricio-hondagneu-simplisafe commented 11 months ago

I ran into this today, I'm surprised this isn't a thing already.

Akindin commented 12 hours ago

Fetching libs like TanstackQuery or RTKQuery expose properties like isFetching, isSuccess, isError which with typeguards can narrow the type to avoid unnecessary checks and usage of !.