Tìm kiếm trang web

typescript - Làm thế nào để định nghĩa một hàm với đối số thứ hai dựa trên kiểu trích xuất từ ánh xạ, dựa trên đối số đầu tiên?


Tôi có một đoạn mã, tương đương với ví dụ này:

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: ''});  

Vấn đề là, trong method, tôi gặp lỗi khi cố gắng tham chiếu đến một thuộc tính, mà dựa trên đối số đầu tiên và câu lệnh if trước đó, lẽ ra nên có sẵn.

Tôi đã có ấn tượng rằng TypeScript nên có khả năng suy luận rằng console.log(p1.fooString); và console.log(p1.notFoo); trong đoạn mã trên thực sự là đúng.

Làm thế nào điều này có thể được giải quyết, mà không cần các chuyển đổi thủ công như:

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);
}

(điều này sẽ thực sự gây ra vấn đề trong trường hợp console.log(p1.notFoo); và một enum với danh sách các thuộc tính dài hơn nhiều?

Câu trả lời #1

Bạn cần làm cho p0 và p1 trở thành một phần của cùng một cấu trúc dữ liệu:

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: '' });  

Sân chơi

Bạn cần đảm bảo rằng trạng thái bất hợp pháp của lập luận không thể trình bày được.

Kiểu Params là một hợp của các trạng thái được phép, đây là lý do tại sao TS có thể suy luận nó

Cách tiếp cận tương tự bạn có thể tìm thấy trong bài viết của tôi

Ví dụ với nạp chồng hàm:

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: '' });  

Sân chơi

Bài viết liên quan