5

I want to create a class in TypeScript, which implements an interface in a way that it is required to have all properties of that interface, even the optional ones, but allows for the optional properties to be undefined.

This is Required_ish<T> type would be wider than Required<T>, but stricter than T, because it would require me to explicitly list all the properties. However the same values would be assignable to the parameters of Required_ish<T> and T.

I've tried this but it seems to do the same thing as Required:

type Required_ish<T> = T & { [K in keyof T]-?: T[K] | undefined }

The desired properties:

interface Foo {
    a: string;
    b?: number;
}

class Bar1 implements Foo               { a = ''; } // allowed
class Bar2 implements Required_ish<Foo> { a = ''; } // error
class Bar3 implements Required<Foo>     { a = ''; } // error

class Bar4 implements Foo               { a = ''; b = undefined; } // allowed
class Bar5 implements Required_ish<Foo> { a = ''; b = undefined; } // allowed
class Bar6 implements Required<Foo>     { a = ''; b = undefined; } // error

class Bar7 implements Foo               { a = ''; b = 0; } // allowed
class Bar8 implements Required_ish<Foo> { a = ''; b = 0; } // allowed
class Bar9 implements Required<Foo>     { a = ''; b = 0; } // allowed
m93a
  • 8,866
  • 9
  • 40
  • 58

1 Answers1

7

I finally found a solution. And it's more readable than I expected it to be.

type Required_ish<T> =
{
    [K in keyof Required<T>]: T[K]
};

Ok, but why does this work? Let's break this down.

First, I start with a homomorphic mapped type:

type Required_ish<T> = { [K in keyof T]: T[K] }

Mapped types are a feature that allows you to modify a type but maintain its internal structure (a tuple will still be a tuple) and property modifiers (readonly, optional) unless you're explicit about it. The line I just wrote is more-or-less an identity type: for each key of T it returns the same type and modifiers as T had.

Now there are two modifiers that allow changing the requiredness of a property:

{ [K in keyof T]?:  T[K] } // add the optional modifier
{ [K in keyof T]-?: T[K] } // remove the optional modifier

It might be tempting to use -? here, but sadly this modifier does two things. Not only it removes the optional modifier from the property, it also removes undefined from the resulting type. This means that whenever you use -?, you have no way to return a type that extends undefined.

Luckily, we can get the keys not from T, but from Required<T>. As the compiler takes all the modifiers from the left-hand side (the part before :), the property will have the same modifiers as Required<T>[K]. This means that it will never be optional and the readonly modifier will stay unchanged. And because we haven't used the -? modifier, T[K] will stay intact and possibly undefined.

This way we effectively bypassed the side-effect of -? and the resulting type does exactly what I asked.

m93a
  • 8,866
  • 9
  • 40
  • 58