Search code examples
typescriptunion-types

typescript interface with array union type


Seems that this flags array for union type does not really work for me. I have been playing with idea that implementing class must provide flags about interfaces it's implementing which is also easy to get out from implementing class. Flags as flags: Record<T & FeatureFlagType, true> works, but this also doable with array context? I have tried with all different combinations for flags like (T & FeatureFlag)[] but nothing seem to work.

type FeatureFlagType = 'feature1' | 'feature2';

interface FeatureFlag<T extends FeatureFlagType> {
    flags: T[];
    flagsRecords: Record<T, true>;
}

interface Feature1 extends FeatureFlag<'feature1'> {
    doFeature1: () => void;
}

interface Feature2 extends FeatureFlag<'feature2'> {
    doFeature2: () => void;
}

class Instance implements Feature1, Feature2 {
    public flags = ['feature1' as 'feature1', 'feature2' as 'feature2']; // not ok
    public flagsRecords = {feature1: true as true, feature2: true as true}; // this is ok
    public doFeature1() {

    }
    public doFeature2() {

    }
}

Playground


Solution

  • implements enforces an intersection of types. And the problem here is that:

    'a'[] & 'b'[] // never
    

    So this will not work. What you want is a union of strings for the flags array, but implements will not do that for you.

    However an intersection will work with records:

    Record<'a', string> & Record<'b', string> // { a: string, b: string }
    

    Here's an alternative approach:

    // See: https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type
    type UnionToIntersection<U> = 
      (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
    
    type FeatureFlagType = 'feature1' | 'feature2';
    
    interface FeatureFlag<T extends FeatureFlagType> {
        flag: T;
    }
    
    type HasFeatures<T extends FeatureFlag<FeatureFlagType>[]> =
        { flags: T[number]['flag'][] }
        & UnionToIntersection<Omit<T[number], 'flag'>>
    
    
    // intended usage:
    class A implements HasFeatures<[Feature1, Feature2]> {
      //...
    }
    

    Now each feature has a single flag. The HasFeatures accepts an array of features, and makes an array of all possible features as the flags property.

    Then all non flag properties and intersected together and merged with the type, so the types gets all feature interfaces.

    (In order to get the intersection of the array of feature interfaces, some voodoo is required)

    But now this works as you would expect:

    
    interface Feature1 extends FeatureFlag<'feature1'> {
        doFeature1: () => void;
    }
    
    interface Feature2 extends FeatureFlag<'feature2'> {
        doFeature2: () => void;
    }
    
    class Instance implements HasFeatures<[Feature1, Feature2]> {
        public flags = [
            'feature1' as const,
            'feature2' as const
        ];
        public doFeature1() {
    
        }
        public doFeature2() {
    
        }
    }
    

    See playground