王玉略
王玉略

Reputation: 3322

Typescript: how to correctly do type inference in union types?

I defined a union type called IConfigFactory, it accepts a parameter of type IConfig or returns a function of type IConfig

but it not do type inference correctly, like as follows:

interface IViteConfig {
  vite?: true;
}

interface IWebpackConfig {
  vite?: false;
  plugins: string[];
}

type IConfig = IViteConfig | IWebpackConfig;

type IConfigFactory = (() => IConfig) | IConfig;

function defineConfig(config: IConfigFactory) {
  return config;
}

defineConfig({
  vite: true,   // Not prompting as expected
  plugins: [],
});

type IConfigFactory2 = IConfig;

function defineConfig2(config: IConfigFactory2) {
  return config;
}

defineConfig2({
  vite: true,
  plugins: [], // Prompting as expected
});

enter image description here

enter image description here

ts playground

but when I defined IConfigFactory only accepts a parameter of type IConfig, it works well.

What am I doing wrong and how can I fix it?

Upvotes: 1

Views: 214

Answers (1)

Andrew E
Andrew E

Reputation: 8327

Edit

Based on comments, use an explicit discriminator rather than relying on vite being true or false. If you think about it, that approach is fragile anyway, as it defines all config in terms of vite. Maybe you want to add Rollup or Tsup, etc.

interface ViteConfig {
  kind: 'vite'
}

interface WebpackConfig {
  kind: 'webpack'
  plugins: string[]
}

Result: enter image description here

Playground link

Original

TypeScript discriminated unions work by being specific about a 'discriminator'. In your case, vite:

interface IViteConfig {
  vite: true;                  // <--- NOTE
}

interface IWebpackConfig {
  vite: false;                 // <--- NOTE
  plugins: string[];
}

type IConfig = IViteConfig | IWebpackConfig;

type IConfigFactory = (() => IConfig) | IConfig;

function defineConfig(config: IConfigFactory) {
  return config;
}

defineConfig({ vite: true });               // ok

defineConfig({ vite: false, plugins: [] }); // ok

(playground link)

As a side note, the I prefix, commonly used in old school WIN32 and C#-land is overkill in TypeScript.

The same code without the I's is more 'ergnomic' (though this is subjective, of course:

interface ViteConfig {
  vite: true;
}

interface WebpackConfig {
  vite: false;
  plugins: string[];
}

type Config = ViteConfig | WebpackConfig;

type ConfigFactory = (() => Config) | Config;

function defineConfig(config: ConfigFactory) {
  return config;
}

defineConfig({ vite: true });               // ok

defineConfig({ vite: false, plugins: [] }); // ok

Upvotes: 1

Related Questions