Skip to content

Add an absent type forbidding presenceΒ #55143

Open
@RobertSandiford

Description

@RobertSandiford

Suggestion

πŸ” Search Terms

type, absent, unset, none, missing, undeclared, not set, undefined

βœ… 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.)
  • [I think so?] This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Add a type (working title absent) that forbids presence of the property or variable.

πŸ“ƒ Motivating Example

The absent type would allow forbidding presence of a property on an object, forbidding a property or method in a derived class, and would allow abstract properties that can be configured to be required or not through generics.

πŸ’» Use Cases

Consider a front end, react-like-framework, using classes.

The absent type combined with a generic would allow toggling between using state, and enforcing type safety on that state (requires initialisation), or not using state, which forbids presence of state. (This is my use case and motivation for this change)

abstract class FrameworkComponent<State extends (object | absent) = absent>{
  abstract state: State
}

class StatelessAppComponent extends FrameworkComponent {
  // specifying state here would violate `absent` type, showing a type error
  render => () => 'Hi - I am a stateless component'
}

type StatefulAppComponentState = {
  message: string
}

class StatefulAppComponent extends FrameworkComponent<StatefulAppComponentState> {
  // omitting state here would violate the requirement of state: StatefulAppComponentState property
  state: StatefulAppComponentState = {
    message: 'Hi - I am a stateful component'
  }
  render => () => this.state.title
}

Here is an example from Next today. The state property is not typesafe, and this code produces a run time error, despite no type errors.

'use client'

import React from 'react'

type Props = object
type State = {
    a: number,
}   

export default class Component extends React.Component<Props, State> {
    render() {
        this.state /// Readonly<State>
        console.log('state.a', this.state.a) // runtime error
        return <p>Component</p>
    }
}

To avoid this the framework would have to require state to be set in every component, with stateless components having to add state: undefined or state = undefined, or create a StatelessComponent variant. This is not the end of the world, it's a bit of an annoyance (seemingly enough of an annoyance that React has sacrificed type safety to avoid it).

export default class StatelessComponent extends React.Component {
    state: undefined
    render() {
        return <p>StatelessComponent </p>
    }
}

Consider a payment processing system, where we want to prevent double processing of orders

type OrderUnpaid = {
  price: number,
  paymentId: absent
}

type OrderPaid = {
  price: number,
  paymentId: string
}

function processPayment(order: OrderUnpaid): OrderPaid {
  return {
    ...order,
    paymentId: 'abcd1234'
  }
}

let order: OrderUnpaid = {
  price: 57
}

// assigning OrderPaid type to OrderUnpaid
order = processPayment(order) // type error, type 'OrderPaid' cannot be assigned to type 'OrderUnpaid', 'paymentId' must be absent in 'OrderUnpaid'

// accidental second payment processing
order = processPayment(order)

//or

const orderPaid = processPayment(order)

// accidental second payment processing
// passing OrderPaid type to function expecting OrderUnpaid
processPayment(orderPaid) // type error, type 'OrderPaid' does not satisfy the constraint 'OrderUnpaid', 'paymentId' must be absent in 'OrderUnpaid'

How this looks in TypeScript today

Playground

type OrderUnpaid = {
  price: number
}

type OrderPaid = {
  price: number,
  paymentId: string
}

function processPayment(order: OrderUnpaid): OrderPaid {
  return {
    ...order,
    paymentId: 'abcd1234'
  }
}

let order: OrderUnpaid = {
  price: 57
}

// assigning a wider object to an existing object is allowed
order = processPayment(order)

// accidental second payment processing
processPayment(order) // allowed, application bug

//or

const orderPaid = processPayment(order)

// accidental second payment processing
// passing a wider object to a function is allowed
processPayment(order) // allowed, application bug

An Order type with an option paymentId does not improve safety

type Order = {
  price: number
  paymentId?: string
}

undefined and ?: syntax

{ prop: undefined } requires prop to be present

{ prop?: 'foo' } allows prop to be absent, or set to undefined

This suggests some concept of absent already exists in TS.

Therefore, ?: T would equate to T | undefined | absent

absent variables

const foo: absent would probably not be allowed. const foo in javascript creates the variable, so this would be an oxymoron. This could be treated as a developers aid, being allowed, and emitting no javascript, but I don't see a use case, so its easier not to.

absent properties on the global/window object should I think prevent those variables being declared with var. I expect that this will very rarely be used, but it should really be enacted for consistency.

⇄ Relations to other issues

Exact types #12936 - this issue solves some of the same issues that Exact types could be used to solve. Especially in the second example. However in the first example it is serving a different role.

Exact types might rely on absent type, or might be user implementable using an absent type.

πŸ› οΈ Implementation challenges

I don't see anything super difficult. Unions and the like seem fine. Someone who knows more about TS internals can comment.

I'd be interested to hear how this type and concept would gel with existing code & infrastructure - whether it's something that would fit in naturally or not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions