I want to assign a number to an object's field based on some condition like this:
function maybeANumber(): number | undefined {
const n = Math.random();
return n > 0.5 ? n : undefined;
}
function maybeSetNumber(target: any, field: any) {
const num = maybeANumber();
if (num !== undefined) {
target[field] = num;
}
}
This works because I used any
liberally, but how can I type this correctly, so that it detects type errors:
interface Foo {
a: string,
b: number,
}
const foo: Foo = { a: "", b: 0 };
maybeSetNumber(foo, "a"); // Should be a compile-time error.
maybeSetNumber(foo, "b"); // Should be a ok.
Is there any way to do this?
Edit: Important clarification: the field names are static. I don't need it to work with arbitrary strings. I've tried a load of stuff with keyof
but couldn't quite figure it out.
The Type field provides choices that control the effect that editing work, assignment units, or duration has on the calculations of the other two fields. The options are: The Type field also provides choices for the resource type. The choices are Work and Material. Work resources are people and equipment.
The Type field provides choices that control the effect that editing work, assignment units, or duration has on the calculations of the other two fields. The options are: The Type field also provides choices for the resource type.
(For this discussion, type safety specifically refers to memory type safety and should not be confused with type safety in a broader respect.) For example, type-safe code cannot read values from another object's private fields. Robin Milner provided the following slogan to describe type safety: Well-typed programs cannot "go wrong".
This article explains how C# type safety and how .NET helps write safe code. Type safety in .NET has been introduced to prevent the objects of one type from peeking into the memory assigned for the other object. Writing safe code also means to prevent data loss during conversion of one type to another.
You can use generics in your maybeSetNumber()
signature to say that field
is of a generic property key type (K extends PropertyKey
), and target
is of a type with a number
value at that key (Record<K, number>
using the Record
utility type):
function maybeSetNumber<K extends PropertyKey>(target: Record<K, number>, field: K) {
const num = maybeANumber();
if (num !== undefined) {
target[field] = num;
}
}
This will give the behaviors you want:
maybeSetNumber(foo, "a"); // error!
// ----------> ~~~
// Types of property 'a' are incompatible.
maybeSetNumber(foo, "b"); // okay
Warning: TypeScript isn't perfectly sound, so this will still let you do some unsafe things if you start using types which are narrower than number
:
interface Oops { x: 2 | 3 }
const o: Oops = { x: 2 };
maybeSetNumber(o, "x"); // no error, but could be bad if we set o.x to some number < 1
It is also possible to make the signature such that the error above is on "a"
and not on foo
. This way is more complicated and requires at least one type assertion since the compiler doesn't understand the implication:
type KeysMatching<T, V> = { [K in keyof T]: V extends T[K] ? K : never }[keyof T]
function maybeSetNumber2<T>(target: T, field: KeysMatching<T, number>) {
const num = maybeANumber();
if (num !== undefined) {
target[field] = num as any; // need a type assertion here
}
}
maybeSetNumber2(foo, "a"); // error!
// ----------------> ~~~
// Argument of type '"a"' is not assignable to parameter of type '"b"'.
maybeSetNumber2(foo, "b"); // okay
This doesn't suffer from the same problem with Oops
,
maybeSetNumber2(o, "x"); // error!
but there are still likely edge cases around soundness. TypeScript often assumes that if you can read a value of type X
from a property then you can write a value of type X
to that property. This is fine until it isn't. In any case either of these will be better than any
.
Playground link to code
I would type maybeSetNumber
like this:
function maybeSetNumber<F extends string>(target: { [key in F]: number }, field: F) {
const num = maybeANumber();
if (num !== undefined) {
target[field] = num;
}
}
Playground link
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With