-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typescript Convention: How should we implement enumerations with TypeScript? #1106
Comments
Here's an example of how to map between a string array and a string union type: https://github.com/phetsims/gravity-and-orbits/blob/1652582854fe6bb0ad5f48d748d6f28776474933/js/common/model/GravityAndOrbitsBodies.ts // DRY string union values and union type: https://stackoverflow.com/questions/44480644/string-union-to-string-array
const GravityAndOrbitsBodies = [ 'planet', 'satellite', 'star', 'moon' ] as const;
gravityAndOrbits.register( 'GravityAndOrbitsBodies', GravityAndOrbitsBodies );
export default GravityAndOrbitsBodies;
export type GravityAndOrbitsBodiesType = ( typeof GravityAndOrbitsBodies )[number]; |
The reason it seemed we needed the this.pointAddedEmitter = new Emitter( {
parameters: [
{ valueType: Vector2 },
{ validValues: GravityAndOrbitsBodies }
]
} );
this.pointRemovedEmitter = new Emitter( { parameters: [ { validValues: GravityAndOrbitsBodies } ] } );
this.clearedEmitter = new Emitter( { parameters: [ { validValues: GravityAndOrbitsBodies } ] } );
this.userModifiedPositionEmitter = new Emitter();
this.userModifiedVelocityEmitter = new Emitter(); However, with TypeScript, we don't need to validate those values because the type checker does that. Therefore, we could use a pattern more like this: this.pointAddedEmitter = new Emitter2<Vector2, GravityAndOrbitsBodies>();
this.pointRemovedEmitter = new Emitter1<GravityAndOrbitsBodies>();
this.clearedEmitter = new Emitter1<GravityAndOrbitsBodies>();
this.userModifiedPositionEmitter = new Emitter0();
this.userModifiedVelocityEmitter = new Emitter0(); This will allow type checking on the emits and listener callbacks. TypeScript is not yet approved for common code, so investigations along this line would need to be in a branch. With that pattern, a type union enum would be defined the idiomatic TypeScript way: type GravityAndOrbitsBodies = 'planet' | 'satellite' | 'star' | 'moon';
export default GravityAndOrbitsBodies; |
But I feel we cannot throw away the valid values completely, at least for phet-io usage. When PhET-iO creates radio buttons, it is easier to get the list of valid values for an enum from runtime data, and it cannot (easily?) use type information. So in those cases, we may need to use something like #1106 (comment) at the minimum. Self-unassigning for now. |
Our legacy pattern can get TypeScript completion and support by changing from const TimeSpeed = Enumeration.byKeys( [ 'FAST', 'NORMAL', 'SLOW' ] ); to const TimeSpeed = new Enumeration( { keys: [ 'FAST', 'NORMAL', 'SLOW' ] } );
TimeSpeed.NORMAL = TimeSpeed.NORMAL; // eslint-disable-line
TimeSpeed.FAST = TimeSpeed.FAST; // eslint-disable-line
TimeSpeed.SLOW = TimeSpeed.SLOW; // eslint-disable-line Oops, it fails at runtime due to lack of reassignment. |
Simply changing: const TimeSpeed = Enumeration.byKeys( [ 'FAST', 'NORMAL', 'SLOW' ] ); to const TimeSpeed = new Enumeration( { keys: [ 'FAST', 'NORMAL', 'SLOW' ] } ); passes the type checker, but doesn't offer any kind of type safety. But maybe that's all we need until we have common code support for enums? Or maybe I should just eagerly create a TypeScript-appropriate pattern for enums that has phet-io support. |
A proposal for enumerations, based on progressive enhancements: Level 1. String Literal UnionWhen you just need to specify one of a few choices, and don't need rich features or PhET-iO, use a string literal union like BodyTypeEnum.ts https://github.com/phetsims/gravity-and-orbits/blob/9322b9a0b9fdcfd4fb2139230a2f338549835b19/js/common/model/BodyTypeEnum.ts type BodyTypeEnum = 'planet' | 'satellite' | 'star' | 'moon';
export default BodyTypeEnum; Level 2: Getting values at runtime for a string literal union.If you also need to get the values at runtime, for validValues or for a // The values
const CircuitElementViewTypeValues = [ 'lifelike', 'schematic' ] as const;
// The string literal union type
type CircuitElementViewType = ( typeof CircuitElementViewTypeValues )[number];
// Register the values available at runtime. Note it does not match the filename
circuitConstructionKitCommon.register( 'CircuitElementViewTypeValues', CircuitElementViewTypeValues );
// Export
export { CircuitElementViewType as default, CircuitElementViewTypeValues }; Level 3: Rich enumeration objects.If you want methods and other rich functionality, you could define it like so (I ported XDirection from natural selection): // Copyright 2020-2021, University of Colorado Boulder
/**
* XDirection is the direction that an Organism (bunny, wolf, shrub) is facing along the x axis.
*
* @author Chris Malley (PixelZoom, Inc.)
*/
import dotRandom from '../../../dot/js/dotRandom.js';
import circuitConstructionKitCommon from '../circuitConstructionKitCommon.js';
class XDirection {
sign: number;
opposite: XDirection | null;
static LEFT: XDirection;
static RIGHT: XDirection;
static getRandom: () => XDirection;
static KEYS: readonly [ 'LEFT', 'RIGHT' ];
static phetioDocumentation: string;
static VALUES: readonly XDirection[];
constructor( sign: number ) {
this.sign = sign;
this.opposite = null;
}
}
XDirection.LEFT = new XDirection( -1 );
XDirection.RIGHT = new XDirection( +1 );
XDirection.KEYS = [ 'LEFT', 'RIGHT' ] as const;
XDirection.VALUES = [ XDirection.LEFT, XDirection.RIGHT ] as const;
XDirection.getRandom = () => dotRandom.nextBoolean() ? XDirection.RIGHT : XDirection.LEFT;
XDirection.phetioDocumentation = 'hello';
circuitConstructionKitCommon.register( 'XDirection', XDirection );
export default XDirection; And the IO type that makes it work in studio would be something like: // Copyright 2018-2021, University of Colorado Boulder
/**
* IO Type for phet-core Enumeration that supports serializing and deserializing values. Cannot be moved to the core
* type since Enumeration must be defined before ValidatorDef can be defined.
*
* @author Sam Reid (PhET Interactive Simulations)
*/
import IOType from '../../../tandem/js/types/IOType.js';
import StateSchema from '../../../tandem/js/types/StateSchema.js';
// {Map.<enumeration:Enumeration, IOType>} - Cache each parameterized RichEnumuerationIO so that it is only created once.
const cache = new Map();
type RichEnumeration<U> = {
KEYS: readonly string[]
VALUES: readonly U[]
phetioDocumentation: string
};
/**
* This caching implementation should be kept in sync with the other parametric IO Type caching implementations.
* @param {Object} enumeration
* @returns {IOType}
*/
const RichEnumuerationIO = <U, T extends RichEnumeration<U>>( enumeration: T ) => {
if ( !cache.has( enumeration ) ) {
const values = enumeration.VALUES;
const getKey = ( value: T ) => enumeration.KEYS.find( key =>
// @ts-ignore
enumeration[ key ] === value );
// Enumeration supports additional documentation, so the values can be described.
const additionalDocs = enumeration.phetioDocumentation ? ` ${enumeration.phetioDocumentation}` : '';
cache.set( enumeration, new IOType( `RichEnumuerationIO(${enumeration.KEYS.join( '|' )})`, {
validValues: values,
documentation: `Possible values: ${enumeration.KEYS.join( ', ' )}.${additionalDocs}`,
toStateObject: getKey,
fromStateObject: ( stateObject: string ) => {
assert && assert( typeof stateObject === 'string', 'unsupported RichEnumuerationIO value type, expected string' );
assert && assert( enumeration.KEYS.indexOf( stateObject ) >= 0, `Unrecognized value: ${stateObject}` );
// @ts-ignore
return enumeration[ stateObject ];
},
stateSchema: StateSchema.asValue( `${enumeration.KEYS.join( '|' )}`, {
isValidValue: ( v: string ) => enumeration.KEYS.includes( v )
} )
} ) );
}
return cache.get( enumeration );
};
export default RichEnumuerationIO; The call site looks like: private readonly directionProperty: Property<XDirection>;
// ...
this.directionProperty = new Property( XDirection.LEFT, {
validValues: XDirection.VALUES,
tandem: tandem.createTandem( 'directionProperty' ),
phetioType: Property.PropertyIO( RichEnumerationIO<XDirection, typeof XDirection>( XDirection ) )
} ); Remaining things to do:
|
Here are 2 ways we can provide typing for legacy enumuerations: Using strings (allows passing 'CATS')( () => {
type AnimalType = 'CATS' | 'DOGS';
type AnimalTypeEnumerationType = {
CATS: AnimalType
DOGS: AnimalType
};
const AnimalTypeEnumeration = Enumeration.byKeys( [ 'CATS', 'DOGS' ] ) as unknown as AnimalTypeEnumerationType;
const property = new Property<AnimalType>( AnimalTypeEnumeration.CATS );
property.set( AnimalTypeEnumeration.MONKEY );
property.set( 'lion' );
property.set( 'CATS' );
property.set( AnimalTypeEnumeration.DOGS );
console.log( property.value );
} )(); Using enums (requires AnimalTypeEnumeration.CATS)( () => {
enum PetKind { //eslint-disable-line
CATS,//eslint-disable-line
DOGS//eslint-disable-line
}
const AnimalType = Enumeration.byKeys( [ 'CATS', 'DOGS' ] ) as unknown as ( typeof PetKind );
const property = new Property<PetKind>( AnimalType.CATS );
property.set( AnimalType.MONKEY );
property.set( 'lion' );
property.set( 'CATS' );
property.set( AnimalType.DOGS );
console.log( property.value );
} )(); We could pick one of these strategies, or one like it, and use it to set up d.ts files next to legacy Enumeration files. I'm not sure how to say Here's a picture showing the underlined type errors: |
I reviewed these proposals with the dev team today, and we decided @zepumph and I should discuss further and bring a proposal to next Thursday's meeting. Questions:
In this code, searching for usages of 'satellite' finds the first case but not the second case. type BodyTypeEnum = 'planet' | 'satellite' | 'star' | 'moon';
const createBody = ( x: BodyTypeEnum ) => {
};
createBody( 'satellite' );
const printName = ( x: string ) => {
console.log( x );
};
printName( 'satellite' ); |
I built a type to automatically set validValues and phetioType: class EProperty<T> extends Property<T> {
constructor( values: readonly T[], value: T, options?: any ) {
options = options || {};
options.validValues = values;
options.phetioType = Property.PropertyIO( StringIO );
super( value, options );
}
}
const p2 = new EProperty<CircuitElementViewType>( CircuitElementViewTypeValues, 'lifelike' );
p2.link( ( e: CircuitElementViewType ) => {
console.log( e );
} );
p2.value = 'lifelike';
p2.value = 'schematic'; |
I moved the examples to wilder and embellished upon them a bit. |
@zepumph and I met about this today. We searched for a strategy that would create a type from an array of strings like For this pattern: class MammalType {
static PUPPY = new MammalType();
static KITTY = new MammalType();
} @zepumph was inclined to make it extend an enumeration type like: class MammalType extends Enumeration{
static PUPPY = new MammalType();
static KITTY = new MammalType();
} But it is unclear to me what problem that would solve. We also discussed that one of the nice parts of the wilder options demo is that it shows a more extreme complexity. We could add another example to the enumeration patterns that shows features like:
We also ran out of time and I wasn't sure we were on the same page about next steps or current status. We will discuss again next week. I feel my perspective here is incomplete, so would be good for @zepumph to chime in about important parts I missed. I also identified Bounds2.EVERYTHING = new Bounds2(...); I think the reason we are having trouble matching TypeScript to our legacy pattern is that our legacy Enumeration class is value-oriented, and we want to promote it to type-oriented. That's why it seems we need a new pattern. |
In case you haven't run across it... https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums:
|
I added a couple more changes. Please review. |
spell TypeScript correctly, phetsims/chipper#1106
spell JavaScript correctly, phetsims/chipper#1106
fix code formatting, phetsims/chipper#1106
add missing part of a sentence, phetsims/chipper#1106
The substance of the documentation looks good, but the execution needed some work -- see commits above. The number of ways that "TypeScript" and "JavaScript" are spelled seems to be a chronic problem. It's (maybe) OK to be fast & loose in comments to yourself. But in public documents, it's distracting, and conveys an absence of attention-to-detail. My 2 cents... I'll keep changing them where I see them. @zepumph feel free to close. |
Makes sense to me! I'll too be on the lookout. Thank you. |
spell TypeScript correctly, phetsims/chipper#1106
spell JavaScript correctly, phetsims/chipper#1106
fix code formatting, phetsims/chipper#1106
add missing part of a sentence, phetsims/chipper#1106
From #1049:
Keep in mind the phet-io support needs.
The text was updated successfully, but these errors were encountered: