Search code examples
typescripttypescript-genericsmapped-types

How to force class to implement properties that can store undefined value


I'm trying to create a mapped type that would force a class to implement a set of properties based on some object type. Basically when object type is extended, class needs to be extended as well.

type SomeObjectType = {
    a?: {/* ... */},
    b?: {/* ... */},
    c?: {/* ... */}
}

type SomeMappedType = {
    [K in keyof SomeObjectType]: SomeGenericClass<K>
}

class SomeClass implements SomeMappedType {
  a?: SomeGenericClass<'a'>;
  b?: SomeGenericClass<'b'>;
  c?: SomeGenericClass<'c'>;
}

The issue with the above code is, that since all object properties in SomeObjectType are optional ? it doesn't force the class to implement them.

I tried to use | undefined but it doesn't work either. Only way to make it work is to get rid of ? using -?:

type SomeMappedType = {
    [K in keyof SomeObjectType]-?: SomeGenericClass<K>
}

class SomeClass implements SomeMappedType {
  a: SomeGenericClass<'a'>;
  b: SomeGenericClass<'b'>;
  c: SomeGenericClass<'c'>;
}

But then I can't store undefined values to those properties.


Solution

  • Is this what you need? I used Array for demonstration purposes.

    type SomeObjectType = {
        a: {/* ... */},
        b?: {/* ... */},
        c?: {/* ... */}
    }
    
    type RequiredLiteralKeys<T> = keyof { [K in keyof T as string extends K ? never : number extends K ? never :
        {} extends Pick<T, K> ? never : K]: 0 }
    
    type OptionalLiteralKeys<T> = keyof { [K in keyof T as string extends K ? never : number extends K ? never :
        {} extends Pick<T, K> ? K : never]: 0 }
    
    type SomeMappedType = {
      [K in RequiredLiteralKeys<SomeObjectType>]: Array<K>
    } & {
      [K in OptionalLiteralKeys<SomeObjectType>]-?: Array<K> | undefined
    }
    
    class SomeClass implements SomeMappedType {
      a!: Array<'a'>;
      b!: Array<'b'>;
      c!: undefined // will error if c is missing but undefined still works
    }
    

    All required properties stay required and all optional will be converted to Array<T> | undefined.

    Playground