Search code examples
typescript

How to specify the types of a function so that the function can accept an object strictly only of type A or strictly only of type B


For example I have such function:

type A = {
    propA1: string
    propA2: string
}

type B = {
    propB1: string
    propB2: string
}

const f = (arg: A | B) => { }

then It can be used like this:

f({propA1: 'val', propA2: 'val', propB1: 'val'})

But I need an error in this case. I would like possibility passed argument with type only A, or only B, without mixing.

Playground


Solution

  • Neither jabba's solution or yours is perfect. In jabaa's it's easy to miss it: Playground, in yours you can provide other properties as undefined, like propB1: undefined.

    So the task is more complex than looks on the surface. Unfortunately I didn't start with premise of allowing overlapped properties. Neither I knew whether any other properties (not belonging to the types) are allowed, so I assumed allowed and rejected only properties from other types. This is generic allowing any number of types. If no other properties are allowed I hope it's easy to tweak.

    So the solution:

    Playground

    type A = {
        propA1: string
        propA2?: string
    }
    
    type B = {
        propB1: string
        propB2: string
    }
    
    type C = {
        propC1: string
        propC2: string
    }
    
    type Union = A | B | C;
    
    
    type IsUnion<T, C extends T = T> = (T extends T ? C extends T ? true : unknown : never) extends true ? false : true;
    
    type Strict<T extends U, U extends object> = IsUnion<U extends unknown ? Extract<keyof U, keyof T> extends never ? never : U : never> extends true ? never : T;
    
    const f = <T extends Union>(arg: Strict<T, Union>) => { }
    
    f({ propA1: 'propA1', prop1: 12}); // ok, a required prop + extra
    f({ propA1: 'propA1', propB1: 'propB1'}); // error, props from several union members
    f({ propA2: 'propA2', propB1: 'propB1'}); // error, no valid union member is provided
    

    So what happens:

    U extends unknown ? Extract<keyof U, keyof T> extends never ? never : U : never - here we check each union member whether it contains any keys from the provided input, Extract<keyof U, keyof T> - if it's never that means there's no overlap of a union member's keys and the input's keys. If there's an overlap we add the union member to the filtered union.

    SO we get the union filtered by the input's key. So we need to make sure that this union is formed from 1 member thus only 1 member provided in the input. For that we use IsUnion utility.

    Thus we ensure that input contains keys only from 1 union member plus any extra keys not found in the union's members.