4

Maybe someone with extended ts knowledge can shed some light on me. I have an object which MAY have some keys - and I want to limit which keys.

In a function I want to initialize a key with a default value (empty array) if it does not exist and then add something to that array.

Typescript keeps complaining the property may be undefined altough thats not logically possible.

Example code:

type keys = "a" | "b" | "c";
let test: {[key in keys]?: string[]} = {};
    
function add(key: keys, value: any){
  test[key] = test[key] || [];
  test[key].push(value);
}

test.push is marked by tsc as "object possibly undefined".

This works neither:

function add(key: keys, value: any){
  test[key] = test[key] || [];
  if(test[key] !== undefined){
      test[key].push(value);
  }
}

Whatever I do - typescript insists the property might be undefined.

Playground

4
  • I put the code in typescript playground as example: typescriptlang.org/play?#code/… Commented Apr 22, 2022 at 13:24
  • 3
    This is a known limitation of TS, see ms/TS#48335; the type of key is not a single string literal, and the compiler doesn't do control flow by following the identity of a variable, just by narrowing its type; if you want to work around this you should save the known-defined thing to its own variable and use it instead of reindexing, like this. Does that address your question fully or is there something I'm missing? Commented Apr 22, 2022 at 13:27
  • It does! At least in the playground and in my narrowed down example. In my real code, it does somehow not work: gist.github.com/Paratron/… See line 29. Commented Apr 22, 2022 at 13:43
  • You need to widen from generic types to specific types there, but if you need that addressed in the answer it should be in your question itself and not just in a comment, in order to prevent scope creep. Commented Apr 22, 2022 at 13:49

1 Answer 1

1

You can use the non-null assertion operator (!) to inform the compiler that the value is not undefined:

TS Playground

type Key = 'a' | 'b' | 'c';
type Test = Record<Key, string[]>;

let test: Partial<Test> = {};

function add (key: Key, value: any) {
  test[key] ??= [];
  test[key]!.push(value);
  //       ^
  // Use non-null assertion operator to inform
  // the compiler that the value is not undefined
}


Bonus: If you also "add" (define) all of the keys in the union, you can then use an assertion function to assert that they're all defined so afterward you no longer have to perform a check when using them:

See related: Required<Type>

TS Playground

type NonNullableValues<T> = { [K in keyof T]-?: NonNullable<T[K]> };

function assertHasValues <T>(obj: T): asserts obj is NonNullableValues<T> {
  for (const key in obj) {
    if (obj[key] == null) {
      throw new Error('Value is null or undefined');
    }
  }
}

type Key = 'a' | 'b' | 'c';
type Test = Record<Key, string[]>;

let test: Partial<Test> = {};

function add (key: Key, value: any) {
  test[key] ??= [];
  test[key]!.push(value);
  //       ^
  // Use non-null assertion operator to inform
  // the compiler that the value is not undefined
}

add('a', 1);
add('b', 2);
add('c', 3);

test.a; // string[] | undefined
test.b; // string[] | undefined
test.c; // string[] | undefined

assertHasValues(test);

test.a; // string[]
test.b; // string[]
test.c; // string[]

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.