-
Notifications
You must be signed in to change notification settings - Fork 13k
Description
Search Terms
properties, object, keys, strict typing
Suggestion
A way to strictly type a object's values whilst maintaining its property keys.
Use Cases
A pattern we end up using a bit in our code is config objects with a constant set of keys, whose values must adhere to a strict structure.
The problem is that the only way I've found to achieve this is by using noop functions, or double variable definitions:
type PropKeyType = string | number | symbol
type Foo<T extends PropKeyType> = {
[k in T] : { someNestedValue : boolean }
}
type SuboptimalFoo = {
[k : string] : { someNestedValue : boolean }
}
function noopTyper<T extends PropKeyType>(x : Foo<T>) : Foo<T> {
return x
}
// strictly types the values, keeps the keys, but requires a noop function
const x = noopTyper({
a: { someNestedValue: true },
b: { someNestedValue: false },
})
type Kx = keyof typeof x // 'a' | 'b'
// strictly types the values, keeps the keys, but requires doubly defining the variable
const yUntyped = {
c: { someNestedValue: true },
d: { someNestedValue: false },
}
const y : Foo<keyof typeof yUntyped> = yUntyped
type Ky = keyof typeof y // 'c' | 'd'
// strictly types the values, but looses the keys
const z: SuboptimalFoo = {
e: { someNestedValue: true },
f: { someNestedValue: false },
}
type Kz = keyof typeof z // string | number
It would be great if there was a way to use this pattern without having to resort to techniques that output additional, useless code that is run at runtime.
The following two definitions would be good, however they both fail hard at compile time.
const a = {
a: { someNestedValue: true },
b: { someNestedValue: false },
} as Foo<keyof typeof a> // Block-scoped variable 'a' used before its declaration.
const b // 'b' is referenced directly or indirectly in its own type annotation.
: Foo<keyof typeof b> = { // Block-scoped variable 'b' used before its declaration.
c: { someNestedValue: true },
d: { someNestedValue: false },
}
This pattern is used regularly with libraries like JSS which require you to declare css class lists for a component at definition time.
Within your component you then can access the class names you defined:
const styles = {
foo: { /* React.CSSProperties */ },
bar: { /* React.CSSProperties */ },
}
interface Props {
classes : Record<keyof typeof styles, string>
}
@withStyles(styles)
class Example extends React.Component<Props> {
public render() {
return (
<div className={this.props.classes.foo}>
<p className={this.props.classes.bar}>
<input className={this.props.classes.baz} />{/* fails as baz is not in keyof typeof styles */}
</p>
</div>
)
}
}
Strictly typing the styles
variable whilst keeping its keys requires one of the above techniques, or you can always be extremely and unmaintainably explicit:
const styles = {
foo: { /* React.CSSProperties */ } as React.CSSProperties,
bar: { /* React.CSSProperties */ } as React.CSSProperties,
}
Checklist
My suggestion meets these guidelines:
- 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. new expression-level syntax)