George Panainte
George Panainte

Reputation: 13

Map interface keys to an array of key value pairs

Given some interface

E.g.

interface Person {
    name: string;
    age: number ;
}

I want to create a generic function that accepts the following arguments


const res = Result.combineValues<Person>(
    { age: 18 },
    { name: 'John Doe' }
);

So far I have


class Result<T> {

    readonly value: T;

    private constructor(value: T) {
        this.value = value;
    }

    public static combineValues<T>(...results: { [key in keyof T]?: Result<T[key]> | T[key] }[]): Result<T> {
        let value: T;
        // ... compute value
        return new Result<T>(value);
    }
}


but the issue is that it allows for undefined values


const res = Result.combineValues<Person>(
    { age: undefined }, // this should give a compile error because age should be a number or Result<number>
    { name: 'John Doe' }
);

and it does not validate that all the properties were defined


const res = Result.combineValues<Person>(
    { age: 18 } 
    // should give a compile error because `{name: 'Some Name'}` is missing from argument list
); 

Upvotes: 1

Views: 1178

Answers (1)

Mack
Mack

Reputation: 771

Typescript doesn't have any method for breaking down a union or intersection type into its constituents, or specifying that a number of unknown types must "add up" to a known type.

For this reason, making the function generic on the Result type and trying to enforce a condition on the variadic argument tuple isn't going to work.

Instead: we make the function generic on the tuple of types it is passed, and infer the return type of the function based on the intersection of all those types. Then, you can enforce compile-time constraints by assigning the return value of the function to a variable of type Result<Person>, which will make sure the assignment is valid.

To allow for instances of the Result class, we also specify a type to replace all members of a type that are Result<T> with T.

type ReplaceResultsIn<Obj> = {
  [Key in keyof Obj]: Obj[Key] extends Result<infer T> ? T : Obj[Key]
}
type Combine<T extends unknown[]> = T extends [infer First, ...infer Rest] ? (ReplaceResultsIn<First>) & Combine<Rest> : unknown

We then type the combineValues function to return Combine< the argument types >:

class Result<T> {
  readonly value: T;

  public constructor(value: T) {
    this.value = value;
  }
  public static combineValues<T extends object[]>(...args: T): Result<Combine<T>> {
        let value!: Combine<T>;
        // ... compute value
        return new Result<Combine<T>>(value);
    }
}

Consumers of this class can now exist with type-safety:

interface Person {
  name: string;
  age: number;
};

// these are all OK
const person: Result<Person> = Result.combineValues({age: new Result(21)}, {name: ""});
const person2: Result<Person> = Result.combineValues({age: 21, name: ""});
const person3: Result<Person> = Result.combineValues({age: 21}, {name: ""});
const person4: Result<Person> = Result.combineValues({name: new Result("")}, {age: new Result(21)});

// these are all bad
const person5: Result<Person> = Result.combineValues({});
const person6: Result<Person> = Result.combineValues({age: 21});
const person7: Result<Person> = Result.combineValues({name: 21}, {age: 21});
const thing = Result.combineValues("");
const thing2 = Result.combineValues(null);

Playground link

Upvotes: 1

Related Questions