Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ensure existance of optional property in typescript interface

Tags:

typescript

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?

like image 995
gandalfml Avatar asked Sep 01 '18 10:09

gandalfml


3 Answers

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 
    }
}
like image 64
Titian Cernicova-Dragomir Avatar answered Sep 24 '22 21:09

Titian Cernicova-Dragomir


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

like image 37
Emaro Avatar answered Sep 24 '22 21:09

Emaro


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>
    }
}
like image 25
Wilt Avatar answered Sep 22 '22 21:09

Wilt