2

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.

jamacku
  • 35
  • 6
  • 1
    if you do `class SomeClass implements SomeType`, TypeScript will complain if a,b or c are missing. Isn't that what you want? – Tobias S. May 04 '22 at 12:51
  • @TobiasS. Yes, It is what I want. But from what I saw, it didn't work that way in my case. – jamacku May 04 '22 at 12:59
  • @TobiasS. Here is my real code example: https://github.com/jamacku/plumber/blob/55da4ea1ac5d57eb29b15c8e6176f67526d64120/src/models/config/rules/rules.model.ts – jamacku May 04 '22 at 13:06
  • @TobiasS. You were right, thank you! Issue with my code is that all properties in the object can be `undefined` so I'm forced to use `-?` in my mapped type in order to achieve expected behavior. (https://stackoverflow.com/a/53834559/10221282) But then I'm not able to assign undefined to those properties. – jamacku May 04 '22 at 13:27
  • you can assign `undefined` to it, if you make the property optional `blocking?: boolean` in the class where you implement `RulesProperties` – Tobias S. May 04 '22 at 14:30
  • @TobiasS. Could you please provide me with some example? I'm not sure what you mean by your last suggestion. Thank you. Also I updated the question. – jamacku May 05 '22 at 07:06

2 Answers2

2

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

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • Thank you so much, It works perfectly. Could you please add some notes about `RequiredLiteralKeys` and `OptionalLiteralKeys` types, I'm a bit confused about what they do. Thank you. – jamacku May 05 '22 at 08:05
  • 1
    They get all required and all optional keys respectively. See here https://stackoverflow.com/a/52991061/8613630 – Tobias S. May 05 '22 at 08:07
1

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

Dave Meehan
  • 3,133
  • 1
  • 17
  • 24