Is there a way to have typescript throw error when a key does not exist?

Viewed 384

as an example

const { foo, bar, bruno } = lala(['foo', 'bar']);

function lala(keys: string[]) {
    return keys.reduce((withIds, key) => ({
        ...withIds,
        [key]: 'some-new-value'
    }), {} as Record<string, string>);
}
    
console.log(foo, bar, bruno); // some-new-value some-new-value undefined

I'm trying to make typescript (under VSCode) to wiggle the bruno variable, as it's not in the array

Is there a special trick / function for such action?

4 Answers

It is possible to do:

const arr = ['foo', 'bar'] as const;

type Values<T extends ReadonlyArray<string>> = T[number]

type Reduced<T extends ReadonlyArray<string>> = {
  [P in Values<T>]: string
}

function lala<T extends ReadonlyArray<string>>(keys: T) {
  return keys.reduce((withIds, key) => ({
    ...withIds,
    [key]: 'some-new-value'
  }), {} as Reduced<T>);
}

const result =  lala(arr); // Reduced<readonly ["foo", "bar"]>
const { foo, bar, bruno } = lala(arr); // expected error


Playground

My answer overlaps a lot with @captain-yossarian's but it's without the additional type definitions

const { foo, bar, bruno } = lala(['foo', 'bar'] as const);

function lala<K extends readonly string[]>(keys: K): Record<K[number], string> {
    return keys.reduce((withIds, key) => ({
        ...withIds,
        [key]: 'some-new-value'
    }), {} as Record<K[number], string>);
}

Playground

A part of this can actually be done using template types:

function foo<ResultType, KeyType extends keyof ResultType>(keys: KeyType[]): ResultType {
    return keys.reduce((withIds, key) => ({
        ...withIds,
        [key]: 'some-new-value',
    }), {} as ResultType);
}

const {a, b, c} = foo(['a', 'b']);

i am not sure if VSCode will pick up the missing key though, since i don't think that this would throw the compiler off, as the generic doesn't actually know the result type (it's just interpolating what it does know, which is that keys a and b definitely exist).

It would work a lot better if you knew the interface beforehand, then you can do the following:

interface MyInterface {
    a: string;
    b: string;
}

function bar<T extends keyof MyInterface>(keys: T[]): MyInterface {
    return keys.reduce((withIds, key) => ({
        ...withIds,
        [key]: 'some-new-value',
    }), {} as MyInterface);
}

const {a, b, c} = bar(['a', 'b']);

which would raise an compiler error and would as such show up in VSCode

This should work:

const { foo, bar, bruno } = lala(['foo', 'bar']);
//                ^^^^^ <--- Error here

function lala<KeyType extends string>(keys: KeyType[]) {
    return keys.reduce((withIds, key) => ({
        ...withIds,
        [key]: 'some-new-value',
    }), {} as Record<KeyType, any>);
}
    
console.log(foo, bar, bruno); // some-new-value some-new-value undefined

playground link

Related