2

Suppose I have objects as follows:

const a = { propA: 'hello' }
const b = { propB: 'world' }

I want to define an array as follows:

const arr = [ [a, ["propA"], [b, ["propB"] ]

where, in each element (a nested array), the second element is one of the properties of the first element.

I want TS compiler to raise an error when the second element is not a property of the first element, for example, all following should raise an error:

const arr = [
  [a, ["propB"], // propB is a property of b, but not of a
  [a, ["propC"], // propC is not a property of a
  [b, ["propA"], // propA is a property of a, but not of b
  [b, ["propD"]  // propD is not a property of b
]

How do I define a generic type applicable to arr?

2 Answers 2

1

You can do such a check like so

const arr = check([
    [a, ["propA"]],
    [b, ["propB"]],
    [a, ["propB"]], // Type '"propB"' is not assignable to type '"propA"'
    [a, ["propC"]], // Type '"propC"' is not assignable to type '"propA"'
    [b, ["propA"]], // Type '"propA"' is not assignable to type '"propB"'
    [b, ["propD"]]  // Type '"propD"' is not assignable to type '"propB"'
]);
type Entry = readonly [unknown, readonly string[]];

type Check<T extends readonly Entry[]> = {
    [K in keyof T]: Related<T[K]>
};

type Related<T extends Entry> =
    T extends readonly [infer A, unknown]
    ? [A, (keyof A)[]]
    : never

const check = <const T extends readonly Entry[] & Check<T>>(x: T) => x

playground


If you need the inferred type to be mutable, consider doing

type Check<T extends readonly Entry[]> = {
    -readonly [K in keyof T]: Related<T[K]>
//  ----------
};

I realise the question was a little ambiguous, which explains novarx's answer, because there is a syntax error. You could be willing to write

const arr = check([
    a, ["propA"],
    b, ["propB"],
    a, ["propB"], // Type '"propB"' is not assignable to type '"propA"'
    a, ["propC"], // Type '"propC"' is not assignable to type '"propA"'
    b, ["propA"], // Type '"propA"' is not assignable to type '"propB"'
    b, ["propD"]  // Type '"propD"' is not assignable to type '"propB"'
]);

This is also possible to check, but you need to prevent TS from widening the inferred type in different spots so you can work with it:

type Entries = readonly (unknown | string[])[];

type Narrow<T> = { [K in keyof T]: T[K] extends Function ? T[K] : Narrow<T[K]> };

type Check<T> =
    T extends readonly [infer A, infer _, ...infer Rest]
    ? [A, (keyof A)[], ...Check<Rest>]
    : T

const check = <T extends Entries>(x: Check<Narrow<T>>) => x

playground

This version is not as clean as the first one. There is nothing stopping you from writing check([a, ["propA"], b, ["propB"], ['propC']]) because we need to infer the first element of each pair as unknown and we can't enforce that we are dealing with pairs. However, as soon as you add another object after 'propC', the type checker warns you that there is a problem although it's can't quite pinpoint the cause of the problem.

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

Comments

0

It's doable, but due to it's complexity, not recommended (see other Question)

A cleaner approach would be, to wrap the array elements to an appropriate class.

const a = {propA: 'hello'};
const b = {propB: 'world', propBB: 'lorem'};

class ObjectWithProperties<T> {
    constructor(
        public value: T,
        public properties: (keyof T)[]
    ) {}
}

const objectsWithProperties: ObjectWithProperties<any>[] = [
    new ObjectWithProperties(a, ['propA']),
    new ObjectWithProperties(b, ['propB']),
    new ObjectWithProperties(b, ['propB', 'propBB']),
    // Error: Type '"propA"' is not assignable to type '"propB" | "propBB"'
    new ObjectWithProperties(b, ['propA']),
];

The generic type T of value can then be used to assign the type (keyof T)[] to properties, which is an array of strings that are property names of the object in value.

1 Comment

The two questions are not related, there is no notion of alternating patterns, it is simply a tuple of entries.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.