4

I am using typescript@2.3.4.

I have a target object:

interface MyTarget {
    a: string;
    b: string;
    c: string;
    d: string;
}

I want to create multiple converts of partial objects using generics.

One such convert might look like this:

const convert = <T extends object>(t: T): MyTarget => {
    return {
        c: "c",
        d: "d",
        ...t,
    };
};

Yet this yields into:

error TS2698: Spread types may only be created from object types.

even though I guard the generic T to be an object.

I then remember that there is a Partial type, hence I tried this:

const convert = (partial: Partial<MyTarget>): MyTarget => {
    return {
        c: "c",
        d: "d",
        ...partial,
    };
};

Yet the Partial makes all properties optional. I don't want that and it would now throw:

src/Partial.ts(14,5): error TS2322: Type '{ a?: string; b?: string; c: string; d: string; }' is not assignable to type 'MyTarget'.
  Property 'a' is optional in type '{ a?: string; b?: string; c: string; d: string; }' but required in type 'MyTarget'.

I want to create an instance of MyTarget with every field set as an requirement. I do want to keep typesafety, which is why I don't want to this even though it works:

const convert = (partial: Partial<MyTarget>): MyTarget => {
    return {
        c: "c",
        d: "d",
        ...partial,
    } as MyTarget; // loses type checks, really don't want to
};
k0pernikus
  • 60,309
  • 67
  • 216
  • 347

2 Answers2

1

I think you are using an older version of TS (in newer version spread expressions are typed correctly).

Regardless of this, the true issue is that the object literal might not be a full MyTarget. Your code would allow this call

convert({a : "" })// return value is not really MyTarget since it will not contain b

What you really want is the parameter to be MyTarget except c and d:

interface MyTarget {
    a: string;
    b: string;
    c: string;
    d: string;
}
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
const convert = (t: Omit<MyTarget, 'c' | 'd'>) : MyTarget => {
    return {
        c: "c",
        d: "d",
        ...t,
    };
};
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Nice one, shame though that [Exclude only exists since 2.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html). I am currently in no position to upgrade. – k0pernikus Feb 05 '19 at 11:23
  • @k0pernikus Uh ..so really, really old (meaning 1 year +) I belive there is a version of `Omit` for older versions. What version exactly are you on ? – Titian Cernicova-Dragomir Feb 05 '19 at 11:25
  • @k0pernikus here is a version of `Omit` for older versions, should be a drop in replacement, but I have not tested: http://ideasintosoftware.com/typescript-advanced-tricks/ – Titian Cernicova-Dragomir Feb 05 '19 at 11:26
  • The linked Omit type is not behaving as I expected: https://stackoverflow.com/questions/54535539/how-to-create-an-omit-type-for-tsc2-3 – k0pernikus Feb 05 '19 at 13:28
  • The `typeof` approach pointed out here works fine: https://stackoverflow.com/a/48215951/457268 – k0pernikus Feb 05 '19 at 15:50
  • @k0pernikus creative solution if you don't mind the extra runtime stuff just to get the type. Guess I have become too spoiled with mapped and conditional types :) – Titian Cernicova-Dragomir Feb 05 '19 at 15:52
  • 1
    We are still on 2.3 for this project, so no: I don't mind the extra runtime stuff at all. I embrace the performance impact to have another argument for a future upgrade ;) – k0pernikus Feb 05 '19 at 15:54
0

I went with Qwertiy's solution:

interface XYZ {
  x: number;
  y: number;
  z: number;
}

declare var { z, ...xy }: XYZ;

type XY = typeof xy; // { x: number; y: number;}

which works perfectly for tsc@2.3.

k0pernikus
  • 60,309
  • 67
  • 216
  • 347