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

967 Views Asked by At

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.

2

There are 2 best solutions below

2
On BEST ANSWER

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

0
On

Non-optional properties (no ?) must be defined, but through a type union we can specify that they are a specified type or undefined.

interface MandatoryFoo {
  bar: string | undefined
  baz: string | undefined
}

const x: MandatoryFoo = { bar: undefined, baz: undefined }

Optional properties (with ?) can be omitted.

interface OptionalFoo {
  bar?: string | undefined
  baz?: string | undefined
}

const y: OptionalFoo = { bar: undefined }

Optional properties inherently permit undefined, so we don't actually need the union type. You can specify the property as undefined or omit it.

interface OptionalShorthandFoo {
  bar?: string
  baz?: string
}

const z: OptionalShorthandFoo = { bar: undefined }

Playground Link