typescript - Tipe pengembalian tidak benar untuk generik kondisional


Saya sedang mencoba menulis sebuah fungsi yang mengembalikan sebuah nilai menggunakan generik dan tipe kondisional seperti ini:

interface Foo<T> {
  bar: T extends true ? string : undefined;
  id: string;
}

interface Options<T> {
  withBar?: T;
}

function createFoo<T extends boolean>({ withBar }: Options<T>): Foo<T> {
  return {
    id: 'foo',
    ...(withBar && { bar: 'baz' }),
  };
}

Yang di atas menghasilkan kesalahan tipe berikut:

Type '{ bar?: "baz" | undefined; id: string; }' is not assignable to type 'Foo<T>'.
  Types of property 'bar' are incompatible.
    Type '"baz" | undefined' is not assignable to type 'T extends true ? string : undefined'.
      Type 'undefined' is not assignable to type 'T extends true ? string : undefined'.

Bisakah seseorang membantu saya memahami mengapa ini terjadi? Saya menentukan bahwa tipe tersebut bisa tidak terdefinisi, jadi seharusnya diperbolehkan untuk tidak terdefinisi.

Selain itu, saya ingin mendapatkan tipe pengembalian dari fungsi berdasarkan parameter tertentu tanpa benar-benar memanggilnya. Apakah itu mungkin?

misalnya ReturnType<typeof createFoo> tidak akan memberi saya tipe yang benar untuk penggunaan createFoo({ withBar: true }) karena belum mengetahui penggunaan tersebut.

Contoh langsung di sini

Jawaban #1

Ini adalah masalah umum, tipe generik tidak terselesaikan di dalam implementasi fungsi. Anda bisa menulis overload fungsi untuk membuat implementasi Anda bekerja:

interface Foo<T> {
  bar: T extends true ? string : undefined;
  id: string;
}

interface Options<T> {
  withBar?: T;
}

function createFoo<T extends boolean>({ withBar }: Options<T>): Foo<T>
function createFoo({ withBar }: Options<boolean>): Foo<true> | Foo<false> {
  // The implementation works because the return type is allowed to be Foo<true> or Foo<false>
  return {
    id: 'foo',
    ...(withBar && { bar: 'baz' }),
  };
}

// The first function overload is taken because it's the first one to match
// the call parameters
const foo = createFoo({ withBar: false }); // foo is of type Foo<false>

Playground TypeScript

Jawaban #2

Ada perbedaan penting antara properti yang diwajibkan yang dapat menampung nilai atau undefined (kuncinya selalu ada), dan properti opsional (kuncinya mungkin tidak ada sama sekali dalam objek). Lihat exactOptionalPropertyTypes

Dalam postingan Anda, Anda mengetik bar pada Foo selalu hadir, dengan nilai kondisional string atau undefined, tetapi Anda mengembalikan objek yang hanya secara kondisional memiliki kunci bar sama sekali.

Dalam modifikasi di bawah ini, saya mengetik ulang Foo untuk mencerminkan tipe properti opsional untuk bar berdasarkan kondisi boolean yang sama.

Saya juga mengganti sintaks operator spread dengan Object.assign (memuaskan compiler), namun, saya tidak yakin mengapa ini membuat perbedaan.

Juga, yang patut diperhatikan: TS mengeluh jika Anda mencoba menyebarkan tipe non-objek ke dalam sebuah objek (misalnya undefined dalam postingan Anda):

const obj = {...undefined};
/*           ^^^^^^^^^^^^
Error: Spread types may only be created from object types.(2698) */

Playground TS

type StringId = { id: string };

type Foo<T> = T extends true ? StringId & { bar: string } : StringId;

interface Options<T> {
  withBar?: T;
}

function createFoo<T>({ withBar }: Options<T>): Foo<T> {
  return Object.assign({id: 'foo'}, (withBar && { bar: 'baz' }));
}

console.log(createFoo({withBar: true})); // { id: "foo", bar: "baz" }
console.log(createFoo({withBar: false})); // { id: "foo" }
console.log(createFoo({})); // { id: "foo" }

Selain itu, saya ingin mendapatkan tipe pengembalian dari fungsi berdasarkan parameter tertentu tanpa benar-benar memanggilnya. Apakah itu mungkin?

Ya, dengan menambahkan tipe pengembalian kondisional pada fungsi, yang bergantung pada parameternya:

Playground TS

function createFoo<T extends boolean>({ withBar }: Options<T>): T extends true ? Foo<true> : Foo<false> {
  return Object.assign({id: 'foo'}, (withBar && { bar: 'baz' }));
}

const foo1 = createFoo({withBar: true}); // StringId & { bar: string }
const foo2 = createFoo({withBar: false}); // StringId

Artikel terkait