2

I am having the following type

export type Groups = {
  group1: {
    prop1?: string;
    prop2?: number;
  };
  group2: {
    prop3?: string;
  };
};

I want to define another type to be built based on that group

type AllGroups = {
  groupName: keyof Groups;
  prop: keyof Groups[keyof Groups];
  values: string[];
}[];

Then i define the object

const allGroups: AllGroups = [
  {
    groupName: 'group1',
    prop: 'prop1',
    values: ['a']
  },
];

Problem is typescript is complaining on the prop field. The groupName field is fine.

TS2322: Type 'string' is not assignable to type 'never'. The expected type comes from property 'prop''

I am not sure how to approach this.

keepwalking
  • 2,644
  • 6
  • 28
  • 63

2 Answers2

2

Problem

Consirder following code-

export type Groups = {
  group1: {
    prop1?: string;
    prop2?: number;
  };
  group2: {
    prop3?: string;
  };
};

type Z = Groups[keyof Groups]

type X = keyof Z

Playground

The type Z is a union of all possible types from Groups. The keyof of a union type returns never. That's why type of prop in AllGroups was never

Solution

We create a union type from Groups and then use them as Discriminated Unions

export type Groups = {
  group1: {
    prop1?: string;
    prop2?: number;
  };
  group2: {
    prop3?: string;
  };
};

type AllGroups = {
  [K in keyof Groups]: {
    groupName: K
    prop: keyof Groups[K]
    values: string[]
  }
}[keyof Groups][]

const allGroups: AllGroups = [
  {
    groupName: 'group1',
    prop: 'prop2',
    values: ['a']
  },
  {
    groupName: 'group1',
    prop: 'prop3', // error expected
    values: ['a']
  },
  {
    groupName: 'group2',
    prop: 'prop3',
    values: ['a']
  },
  {
    groupName: 'group2',
    prop: 'prop1', // error expected
    values: ['a']
  },
];

Playground

Shivam Singla
  • 2,117
  • 1
  • 10
  • 22
2

Consider next example:

type Result = {
    prop1?: string | undefined;
    prop2?: number | undefined;
} | {
    prop3?: string | undefined;
}

type Keys = keyof Result // never

There is no common keys in above union, hence TS is unable to figure out which keys it should return to you. That's why you are receiving an error.

To handle such kind of unions, you can use @Shivam Singla 's way or this generic way:

// credits goes to Titian Cernicova-Dragomir 
//https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

So, instead of writing:

keyof Groups[keyof Groups];

You can write:

keyof StrictUnion<Groups[keyof Groups]>

Here is my full solution:

export type Groups = {
    group1: {
        prop1?: string;
        prop2?: number;
    };
    group2: {
        prop3?: string;
    };
};

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>


interface AllGroups {
    groupName: keyof Groups;
    prop: keyof StrictUnion<Groups[keyof Groups]>;
    values: string[];
};

const allGroups = <T extends AllGroups, U extends {
    0: ReadonlyArray<T>,
}[
    T['groupName'] extends keyof Groups
    ? T['prop'] extends keyof Groups[T['groupName']]
    ? 0 : never : never]>(arr: ReadonlyArray<T> & U) => arr;

const result = allGroups(
    [{
        groupName: 'group1',
        prop: 'prop2',
        values: ['a']
    },
    {
        groupName: 'group1',
        prop: 'prop1',
        values: ['a']
    }]) // ok

const result2 = allGroups(
    [{
        groupName: 'group1',
        prop: 'prop2',
        values: ['a']
    },
    {
        groupName: 'group1',
        prop: 'prop3', // expected error
        values: ['a']
    }]) // ok

Playground

Drawbacks: here you should use function which literally do nothing except type infering.

I assume it will be optimized by V8 engine.

You can also use closure webpack plugin to get rid of function call.

However, I'm voting for @Shivam Singla 's solution because there is no function overhead.

I just published this solution to give you some alternative way.

This type is a bit complicated:

const allGroups = <T extends AllGroups, U extends {
    0: ReadonlyArray<T>,
}[
    T['groupName'] extends keyof Groups
    ? T['prop'] extends keyof Groups[T['groupName']]
    ? 0 : never : never]>(arr: ReadonlyArray<T> & U) => arr;

But this is the way how I do my validation of function arguments.

If you are interesting in explanation, please ping me, I will update the answer