Home > OS >  Constrain TypeScript generic to a key of a property of another generic
Constrain TypeScript generic to a key of a property of another generic

Time:02-17

I have the following interfaces and object set up:

interface ServicesList {
    [key: string]: Service;
}

interface Service {
    uuid: string;
    characteristics: CharacteristictList;
}

interface CharacteristictList {
    [key: string]: string;
}

const ServiceAndCharacteristicMap: ServicesList = {
    firstService: {
        uuid: '0x100',
        characteristics: {
            characteristicOne: '0x0101',
        },
    },
    secondService: {
        uuid: '0x200',
        characteristics: {
            secondCharacteristic: '0x0201'
        }
    }
};

Then, I have the following function defined:

function sendCharacteristic<T extends Service, K extends keyof T['characteristics']>(props: {
        service: T;
        characteristic: K;
    }) {
        console.log(props.service.uuid, 
                    props.service.characteristics[props.characteristic])
}

Currently, TypeScript complains about this with the following compile-time error:

Type 'K' cannot be used to index type 'CharacteristictList'

My goal is to constrain the second parameter (characteristic) so that I have type safety for which keys I use. For example, the following should succeed:

//should succeed
sendCharacteristic({
    service: ServiceAndCharacteristicMap.firstService,
    characteristic: 'characteristicOne'
});

But, this should fail, since characteristicOne belongs to firstService and I'm passing secondService for the first parameter:

//should fail since characteristicOne belongs to firstService
sendCharacteristic({
    service: ServiceAndCharacteristicMap.secondService,
    characteristic: 'characteristicOne'
});

Right now, neither of the two call sites for sendCharacteristic complain about compilation errors.

How can I correctly constrain the characteristic parameter so that I get type safety for the particular instance of Service that I'm passing in?

TypeScript playground with all included code

CodePudding user response:

The main problem with the code is that you are nor preserving the type of the constant you are assigning to ServiceAndCharacteristicMap

If you completely remove the annotation, the code works as expected. Playground Link

You could also use a function to constrain ServiceAndCharacteristicMap to be ServicesList while preserving the type


function makeServicesList<T extends ServicesList>(o: T){
    return o;
}

const ServiceAndCharacteristicMap = makeServicesList({
    firstService: {
        uuid: '0x100',
        characteristics: {
            characteristicOne: '0x0101',
        },
    },
    secondService: {
        uuid: '0x200',
        characteristics: {
            secondCharacteristic: '0x0201'
        }
    }
});

Playground Link

  • Related