gandalfml
gandalfml

Reputation: 908

Ensure existance of optional property in typescript interface

I have a question about typescript optional properties of interfaces. Assuming the following code:

interface Test {
    prop1: string;
    prop2?: string;
}

function someFunction(data: {prop1: string, prop2: string}) {
    console.log(data.prop1 + ": " + data.prop2);
}

function otherFunction(data: Test) {
    if (data.prop2) {
        someFunction(data); // prop2 might be undefined!
    }
}

and having a strict mode set to true.

Typescript gives me the following error:

Argument of type 'Test' is not assignable to parameter of type '{ prop1: string; prop2: string; }'.
    Property 'prop2' is optional in type 'Test' but required in type '{ prop1: string; prop2: string; }'.

And the question is: why it is like that? Why doesn't typescript understand this if assertion?

First of all, I'd love to understand why? But also some workaround that does not produce any additional runtime code or some tons of type assertion would be a nice to have if it's possible at all?

Upvotes: 6

Views: 15105

Answers (5)

Petr Újezdský
Petr Újezdský

Reputation: 1269

Extending the @TitianCernicova-Dragomir answer with specific checks for undefined and null in case you want to distinguish them.

For undefined guard:

type MakeDefined<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
  { [P in K]-?: Exclude<T[P], undefined> };

export function ensureFieldDefined<T, K extends keyof T>(
  o: T | MakeDefined<T, K>,
  field: K
): o is MakeDefined<T, K> {
  return o[field] !== undefined;
}

For null guard:


type MakeNonNull<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
  { [P in K]: NonNullable<Exclude<T[P], undefined>> };

export function ensureFieldNotNull<T, K extends keyof T>(
  o: T | MakeNonNull<T, K>,
  field: K
): o is MakeNonNull<T, K> {
  return o[field] !== null;
}

Usage

interface Test {
    prop1?: string | null;
}

function otherFunction(data: Test) {
    if (ensureFieldDefined(data, 'prop1')) {
        someFunction(data); // prop1 is now marked as defined (prop1: string | null)
    }

    if (ensureFieldNotNull(data, 'prop1')) {
        someFunction(data); // prop1 is now marked as non-null (prop1?: string)
    }

    if (ensureFieldDefined(data, 'prop1') && ensureFieldNotNull(data, 'prop1')) {
        someFunction(data); // prop1 is now marked as fully mandatory (prop1: string)
    }
}

Thanks again @TitianCernicova-Dragomir for awesome trick :)

Upvotes: 0

Wilt
Wilt

Reputation: 44316

There is a Required built in type in Typescript that does exactly what you want:

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

You can now define a type guard for data is Required<Test>:

const hasRequiredProp2 = (data: Test): data is Required<Test> => {
  return data.hasOwnProperty('prop2');
};

All you need to do is use this test using the type guard like so:

function otherFunction(data: Test) {
    if (hasRequiredProp2(data)) {
        someFunction(data); // Narrowed to Required<Test>
    }
}

Upvotes: 4

Emaro
Emaro

Reputation: 1487

If you want a specific type guard instead of the general solution of Titian Cernicova-Dragomir, you can write a simple function.

interface TestWithProp2 {
    prop1: string;
    prop2: string; // required
}

// Special return type
function isTestWithProp2(x: Test): x is TestWithProp2 {
    return x.prop2 !== undefined;
}

// Use
if (isTestWithProp2(data)) {
    someFunction(data);
}

Source: Typescript: User defined type guards

Upvotes: 2

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

Typescript does understand typeguards like you use, the problem is that they only affect the type of the field not the whole object . So for example under strict null checks we would get the following :

function stringNotUndefined(s: string) {}


function otherFunction(data: Test) {
    stringNotUndefined(data.prop2) // error
    if (data.prop2) {
        stringNotUndefined(data.prop2) //ok
        someFunction(data); // still error
    }
}

We can create a custom type guard that will mark the checked fields as non undefined :

interface Test {
    prop1: string;
    prop2?: string;
}
function someFunction(data: { prop1: string, prop2: string }) {
    console.log(data.prop1 + ": " + data.prop2);
}

type MakeRequired<T,K extends keyof T> = Pick<T, Exclude<keyof T, K>> & {[P in K]-?:Exclude<T[P],undefined> }
function checkFields<T, K extends keyof T>(o: T | MakeRequired<T,K>, ...fields: K[]) : o is MakeRequired<T,K>{
    return fields.every(f => !!o[f]);
}

function otherFunction(data: Test) {
    if (checkFields(data, 'prop2')) {
        someFunction(data); // prop2 is now marked as mandatory, works 
    }
}

Edit

The above version may have a bit too much overhead for such a simple check. We can create a much simpler version for just one field (and use && for more fields). This version has a lot less overhead and might even be inlined if on a hot path.

interface Test {
    prop1?: string;
    prop2?: string;
}
function someFunction(data: { prop1: string, prop2: string }) {
    console.log(data.prop1 + ": " + data.prop2);
}

type MakeRequired<T,K extends keyof T> = Pick<T, Exclude<keyof T, K>> & {[P in K]-?:Exclude<T[P],undefined> }
function checkField<T, K extends keyof T>(o: T | MakeRequired<T,K>,field: K) : o is MakeRequired<T,K>{
    return !!o[field]
}

function otherFunction(data: Test) {
    if (checkField(data, 'prop2') && checkField(data, 'prop1')) {
        someFunction(data); // prop2 is now marked as mandatory, works 
    }
}

Upvotes: 8

Stefano Gogioso
Stefano Gogioso

Reputation: 235

Typescript is statically type-checked, so the type conversion from Test to {prop1:string,prop2:string} must be valid at compile time. The if condition is evaluated at run time instead, so it cannot be used for static type-check analysis (at least not in a trivial way).

It may be possible to envisage ways to enrich Typescript so that guards could be used to allow the kind ot type casting you wish to do, but it's simply not the way it is currently designed to work (and it is more complicated than it might at first seem).

To do what you want to do, you could write a helper function that takes a parameter of type Test and returns one of type {prop1:string,prop2:string}, by filling the optional parameter with some default value if it doesn't have one in the Test parameter.

By the way, you may want to look at the discussion in Setting default value for TypeScript object passed as argument

Upvotes: 0

Related Questions