microsoft/TypeScript

any objs without absorbs unions

mdbetancourt opened this issue ยท 6 comments

๐Ÿ” Search Terms

metadata, any object, any union

โœ… Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

โญ Suggestion

a way to mark an object as "any" without lose all the know type information

๐Ÿ“ƒ Motivating Example

some type as follow would be possible

const obj: unknown & { name: string } = getUser()
// or
const obj: AnyObject & { name: string } = getUser()
//instead of
const obj: { name: string; [k: string]: unknown } = getUser()
// add metadata to objects
type Throws<Err> = {
   [errorSymbol]: Err;
} & uknown
function connectDb(): Db | Throws<ConnectionError> {}

function getUsers(db: Db): User[] {}

getUsers(connectDb()) // currently error because Throws dont have Db properties
connectDb().connect // autocomplete and everything else works well

๐Ÿ’ป Use Cases

could be used to #46142

maybe related with this issue #46548 (dont need an additional type just a way to narrow instead of wide the union when any is present)

or typing some Proxies behaviors without lose type

type AnyObject = {
    [k: string]: never
    [i: number]: never
    [s: symbol]: never
}
type FlexibleObject = { lastGet: Date | null } &  AnyObject; // workaround allow autocomplete but...
function createFlexObject<T>(obj: T): T | FlexibleObject {
  obj['lastGet'] = null
  return new Proxy(
    obj,
    {
      get(target, prop) {
        target.lastGet = new Date()
        return prop in target ? target[prop] : '...'
      },
    }
  );
}
type User = { name: string }
const user: User = { name: '' }
const userWithLastGet = createFlexObject(user)

const targetUser: User = userWithLastGet
//     ^ error Property 'name' is missing in type 'FlexibleObject' but required in type 'User'

i'm using a workaround in https://www.npmjs.com/package/@kraftr/errors to add metadata error to the return

function couldThrow(): string | Throws<Error> & string {} i used a type to convert it to
function couldThrow(): Return<string, Error> {} but with this feature i can use
function couldThrow(): string | Throws<Error>{}

It's very unclear to me what's being proposed here.

It's very unclear to me what's being proposed here.

check line 22 Playground "'connect' is declared here." even when i typed Throws as almost any

@RyanCavanaugh i have another e.g

function defaultize<T>(value: T): Record<string, T> {
  return new Proxy({}, {
    get(target, prop) {
      if(target[prop] !== value) {
        target[prop] = value
      }
      return target[prop]
    }
  })
}

const { name } = defaultize('Jean') // currently with --noUncheckedIndexedAccess name is "string | undefined"

what is my idea?

function defaultize<T>(value: T): Record<string, T> & AnyObject {
....
}
const { name } = defaultize('Jean') // now is just "string" even with --noUncheckedIndexedAccess

The concept of a JS object that appears to give itself any available property seems very rare in practice. I don't think this is something the type system needs to support.

i dont think so, is not so rare, i listed some uses cases:

  • objs with default values (like defaultdict in python)
  • metadata (e.g bring metadata about what errors a function could throw) function fn(): User | Throws<NotExists>
  • an option to allow any in an union without widening the original value e.g User | any result in any i know solutions could be User | User & Record<string, uknown> or User | User & { [k: string]: uknown } but it's awkward specially when there is more than just one type (User & Lead & Contact) | (User & Lead & Contact & Record<string, unknown>) or you just ending doing a type alias
  • i say could be related with #46548 maybe { name: string } |& any could be a solution if that issue is aproved
  • collect variables names from destructuring, i'm creating a package to create cli in a more typed way:
const cli = createCLI()
cli.command('build', buildCmd => {
  const { config, root } = buildCmd.namedArg() // namedArg returns a Proxy with get trap to know the var name
  
  return () => {
    console.log(config.value)
    console.log(root.value)
  }
})
cli.parse(['xx', 'xx', 'build', '--config', 'config.ts', '--root', '.', '--watch']) // error watch is not a possible argument

or request handler with validation

// server.ts
constroller(() => {
  const { name, host } = urlQueries()
  return () => {
    // logic
  }
})
// client.ts
get('http://xxxx/users?name=xx&host=xx')

this last one could be helpful to some frameworks like vue to declare props e.g:
currently in vue 3 to declare props with setup we do

<script setup>
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
</script>

but one possible aproach when all props has the same type could be

<script setup>
const { modelValue } = defineProps(String, '')
</script>

The concept of a JS object that appears to give itself any available property seems very rare in practice

@RyanCavanaugh Are you sure? that concept is precisely the proxy concept there is libs which could use this feature i think you closed this issue too fast there is a lot of uses cases check my previous comment