Description
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
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.