Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type safe field assignment

Tags:

typescript

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.

like image 495
Timmmm Avatar asked Sep 23 '20 19:09

Timmmm


People also ask

How do I use the type field in an assignment?

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.

What is the type field used for?

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.

What is type-safe code?

(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".

How does type safety help write safe code?

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.


2 Answers

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

like image 96
jcalz Avatar answered Nov 04 '22 20:11

jcalz


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

like image 34
GOTO 0 Avatar answered Nov 04 '22 19:11

GOTO 0