Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TS2339: Property does not exist on union type - property string | undefined

I have problem with my union type, which looks like this:

type RepeatForm = {
    step:
        | {
              repeat: false;
          }
        | {
              repeat: true;
              from: undefined;
          }
        | {
              repeat: true;
              from: string;
              by?: string;
          };
};

And I have following function where I want to get value of by if it's there:

export const getByField = (form: RepeatForm) => {
    if (form.step.repeat === false || form.step.from === undefined) {
        return null;
    }
    const x = form.step.from;
    return form.step.by;
};

I get this error: Property 'by' does not exist on type '{ repeat: true; from: undefined; } | { repeat: true; from: string; by?: string | undefined; }'. Property 'by' does not exist on type '{ repeat: true; from: undefined; }'.

Which is super confusing for me, because TypeScript know that form.step.from is different from undefined and he even interpolates type of variable x to string.

What's the reason of this issue? How can I access by property then?

like image 684
Konrad Klimczak Avatar asked Jun 13 '18 15:06

Konrad Klimczak


Video Answer


1 Answers

The original PR for discriminated unions is very specific about the fact that the discriminating field must be a string literal type (with the option to add support for boolean and number literal types which seems to have happened). So your use case where you are discriminating based on the type of the field (string vs undefined) does not appear to be supported. This does not work for example:

let u!: { v: number, n: number } | { v: string, s: string}
if(typeof u.v === 'number') {
    u.n // not accesible, type not narrowed 
}

We could use conditional types and a custom type guard to make things work:

function isUndefined<T, K extends keyof T>(value : T, field: K) : value is Extract<T, { [P in K] : undefined }> {
    return !!value[field]
}

export const getByField = (form: RepeatForm) => {
    if (form.step.repeat === false || isUndefined(form.step, 'from')) {
        return null;
    }
    const x = form.step.from;
    return form.step.by;
};

We can also create a general version of this function that allows narrowing by any type:

type ExtractKeysOfType<T, TValue> = { [P in keyof T]: T[P] extends TValue ? P : never}[keyof T]

function fieldOfType<T, K extends ExtractKeysOfType<T, string>>(value : T, field: K, type: 'string'): value is Extract<T, { [P in K] : string }>
function fieldOfType<T, K extends ExtractKeysOfType<T, number>>(value : T, field: K, type: 'number'): value is Extract<T, { [P in K] : number }>
function fieldOfType<T, K extends ExtractKeysOfType<T, boolean>>(value : T, field: K, type: 'boolean'): value is Extract<T, { [P in K] : boolean }>
function fieldOfType<T, K extends ExtractKeysOfType<T, Function>>(value : T, field: K, type: 'function'): value is Extract<T, { [P in K] : Function }>
function fieldOfType<T, K extends ExtractKeysOfType<T, symbol>>(value : T, field: K, type: 'symbol'): value is Extract<T, { [P in K] : symbol }>
function fieldOfType<T, K extends ExtractKeysOfType<T, object>>(value : T, field: K, type: 'object'): value is Extract<T, { [P in K] : object }>
function fieldOfType<T, K extends ExtractKeysOfType<T, undefined>>(value : T, field: K, type: 'undefined'): value is Extract<T, { [P in K] : undefined }>
function fieldOfType<T, K extends keyof T, TValue extends T[K]>(value : T, field: K, type: new (...args:any[])=> TValue): value is Extract<T, { [P in K] : TValue }>
function fieldOfType<T, K extends keyof T>(value : T, field: K, type: string| Function) :boolean {
    if(typeof type === 'string') {
        return typeof value[field] === type;
    } else {
        return value[field] instanceof type
    }
}

const getByField = (form: RepeatForm) => {
    if (form.step.repeat === false || fieldOfType(form.step, 'from', 'undefined')) {
        return null;
    }
    const x = form.step.from;
    return form.step.by;
};


let u: { v: number, n: number } | { v: string, s: string}={ v: 0, n : 10};
if(fieldOfType(u, 'v', 'number')) {
    console.log(u.n);
}

class A {private a: undefined;}
class B {private b: undefined;}
let uc: { v: A, n: number } | { v: B, s: string} = Math.random() > 0.5 ? { v: new B(), s: '10' } : { v: new A(), n: 10 };
if(fieldOfType(uc, 'v', A)) {
    console.log(uc.n)
}
like image 157
Titian Cernicova-Dragomir Avatar answered Sep 20 '22 23:09

Titian Cernicova-Dragomir