typescript - Bagaimana cara mendefinisikan fungsi dengan argumen kedua berdasarkan tipe yang diekstrak dari pemetaan, berdasarkan argumen pertama?


Saya memiliki kode, setara dengan contoh ini:

export enum Group {
    FOO = 'foo',
    BAR = 'bar',
    BIZ = 'biz'
}

interface Mapping extends Record<Group, any> {
    [Group.FOO]: {fooString: string; fooNumber: number};
    [Group.BAR]: {barString: string; barDate: Date; notFoo: string};
    [Group.BIZ]: {bizBoolean: boolean; bizString: string; notFoo: string};
}

function method<T extends Group>(p0: T, p1: Mapping[T]) {
    if (p0 === Group.FOO) {
        // THE PROBLEM. This fails with: Property 'fooString' does not exist on type
        // '{ fooString: string; fooNumber: number; } | { barString: string; barDate: Date; } | { bizBoolean: boolean; bizString: string; }'.
        // Property 'fooString' does not exist on type '{ barString: string; barDate: Date; }'
        console.log(p1.fooString); 
    } else {
        // THE SAME PROBLEM: Property 'notFoo' does not exist on type 
        // '{ fooString: string; fooNumber: number; } | { barString: string; barDate: Date; notFoo: string; } | { bizBoolean: boolean; bizString: string; notFoo: string; }'.
        // Property 'notFoo' does not exist on type '{ fooString: string; fooNumber: number; }'
        console.log(p1.notFoo);
    }
}

// ok
method(Group.FOO, {fooString: '', fooNumber: 2});

// Fails as expected with: Type 'string' is not assignable to type 'number'. 
// The expected type comes from property 'fooNumber' which is declared here on type '{ fooString: string; fooNumber: number; }' 
method(Group.FOO, {fooString: '', fooNumber: 'test'});

// ok
method(Group.BAR, {barString: '', barDate: new Date(), notFoo: ''});

// Fails as expected with: Type 'number' is not assignable to type 'Date'.
// The expected type comes from property 'barDate' which is declared here on type '{ barString: string; barDate: Date; notFoo: string}'
method(Group.BAR, {barString: '', barDate: 42, notFoo: ''});  

Masalahnya adalah, di dalam method, saya mendapatkan kesalahan saat mencoba merujuk properti, yang berdasarkan argumen pertama dan if sebelumnya, seharusnya tersedia.

Saya memiliki kesan, bahwa TypeScript seharusnya dapat menyimpulkan fakta, bahwa console.log(p1.fooString); dan console.log(p1.notFoo); dalam kode di atas, sebenarnya benar.

Bagaimana hal ini bisa diselesaikan, tanpa konversi manual seperti:

if (p0 === Group.FOO) {
    console.log((p1 as Mapping[Group.FOO]).fooString);
} else {
    console.log((p1 as Mapping[Group.BIZ] | Mapping[Group.BAR] | Mapping[Group.QUX] | Mapping[Group.FRED]).notFoo);
}

(yang akan sangat bermasalah dalam kasus console.log(p1.notFoo); dan sebuah enum dengan daftar properti yang jauh lebih panjang?

Jawaban #1

Anda perlu membuat p0 dan p1 sebagai bagian dari satu struktur data:

export enum Group {
    FOO = 'foo',
    BAR = 'bar',
    BIZ = 'biz'
}

interface Mapping {
    [Group.FOO]: { fooString: string; fooNumber: number };
    [Group.BAR]: { barString: string; barDate: Date; notFoo: string };
    [Group.BIZ]: { bizBoolean: boolean; bizString: string; notFoo: string };
}

type Values<T> = T[keyof T]

type Params = Values<{
    [P in keyof Mapping]: [P, Mapping[P]]
}>

function method(...[p0, p1]: Params) {
    if (p0 === Group.FOO) {
        const test = p1.fooString // string, ok
    } else {
        p1.notFoo // string, ok, as expected because notFoo is shared between two types (BAR, BIZ)
    }
}

// ok
method(Group.FOO, { fooString: '', fooNumber: 2 });

// fails as expected
method(Group.FOO, { fooString: '', fooNumber: 'test' });

// ok
method(Group.BAR, { barString: '', barDate: new Date(), notFoo: '' });

// fail as expected
method(Group.BAR, { barString: '', barDate: 42, notFoo: '' });  

Taman bermain

Anda perlu memastikan bahwa kondisi argumen yang ilegal tidak dapat disajikan.

Tipe Params adalah gabungan dari keadaan yang diperbolehkan, inilah sebabnya TS dapat menafsirkannya

Pendekatan serupa dapat Anda temukan di artikel saya

Contoh dengan overloading fungsi:

export enum Group {
    FOO = 'foo',
    BAR = 'bar',
    BIZ = 'biz'
}

interface Mapping {
    [Group.FOO]: { fooString: string; fooNumber: number };
    [Group.BAR]: { barString: string; barDate: Date; notFoo: string };
    [Group.BIZ]: { bizBoolean: boolean; bizString: string; notFoo: string };
}

type Values<T> = T[keyof T]

type Params = Values<{
    [P in keyof Mapping]: [P, Mapping[P]]
}>

function method(p0: Group.BAR, p1: Mapping[Group.BAR]): Mapping[Group.BAR]['barString']
function method(p0: Group.FOO, p1: Mapping[Group.FOO]): Mapping[Group.FOO]['fooString']
function method(...[p0, p1]: Params) {
    if (p0 === Group.FOO) {
        const test = p1.fooString // string, ok
        return p1.fooString
    } else {
        p1.notFoo // string, ok, as expected because notFoo is shared between two types (BAR, BIZ)
        return p1.notFoo
    }
}

// ok
method(Group.FOO, { fooString: '', fooNumber: 2 });

// fails as expected
method(Group.FOO, { fooString: '', fooNumber: 'test' });

// ok
method(Group.BAR, { barString: '', barDate: new Date(), notFoo: '' });

// fail as expected
method(Group.BAR, { barString: '', barDate: 42, notFoo: '' });  

Taman bermain

Artikel terkait