1

Here is an example.

const a = {
  type: 'file',
  file: 'foo',
}
const b = {
  type: 'emoji',
  emoji: 'bar',
}
const c = {
  type: 'external',
  external: 'baz',
}

The second property is always string. The main problem is using the value of type as a key, using generic.

Would be better if using zod and TypeScript both!

I have tried

type IconType = 'file' | 'emoji' | 'external'

type IconFile<T extends IconType = IconType> = {
  type: T
  [key in T]: string
}

But there is an error: A mapped type may not declare properties or methods

VLAZ
  • 26,331
  • 9
  • 49
  • 67
yuusheng
  • 13
  • 2

3 Answers3

0

The problem with your approach is that mapped types are used to transform the properties of an existing type, not to introduce new properties. You could use an intersection type without default generic parameter for IconFile<T extends IconType> like so:

type IconType = "file" | "emoji" | "external";

type IconFile<T extends IconType> = {
  [K in T]: string;
} & {
  type: T;
};

const a: IconFile<"file"> = {
  type: "file",
  file: "foo"
};
const b: IconFile<"emoji"> = {
  type: "emoji",
  emoji: "bar"
};
const c: IconFile<"external"> = {
  type: "external",
  external: "baz"
};

Playground Link

Behemoth
  • 5,389
  • 4
  • 16
  • 40
0

Another solution:

type Filter<U, T extends Partial<U>> = Extract<U, T>;
type IconType = "file" | "emoji" | "external";

interface FileFile {
  type: Filter<IconType, 'file'>;
  file: 'foo';
}

interface EmojiFile {
  type: Filter<IconType, 'emoji'>;
  emoji: 'bar';
}

interface ExternalFile {
  type: Filter<IconFile, 'external'>;
  external: 'baz';
}

type IconFile = FileFile | EmojiFile | ExternalFile;
alex87
  • 419
  • 3
  • 11
0

There are 2 problems to solve:

Problem 1: Mapped type with extra properties

You need to use intersection type:

type IconType = 'file' | 'emoji' | 'external'

type IconFile<U extends IconType & string> = { type: U } & { [P in U]: string }

const x: IconFile<'file'> = {
  type: 'file',
  file: 'string'
}

Note the extra generic argument on IconFile of type string.

Problem 2: Map union type to another union type

We'd like to get a type equal to IconFile<'file'> | IconFile<'emoji'> | IconFile<'external'>

You can use conditional type for distributing over the members of the union type

type DistributeToIconFile<U extends IconType & string> = U extends string 
  ? IconFile<U> 
  : never;
  
type IconFileAsUnion = DistributeToIconFile<IconType>
    // ^? 
    // type IconFileAsUnion = IconFile<'file'> | IconFile<'emoji'> | IconFile<'external'>

const a: IconFileAsUnion = {
  type: 'file',
  file: 'myfile'
};

See TypeScript: Map union type to another union type

Note that simple IconFile<IconType> does not distribute the union members

const nodDistributedBadApproach: IconFile<IconType> = {
  type: 'file',
  file: 'string1',
  emoji: 'string2',
  external: 'string3'
}

Playground

Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • So nice of you! That's exactly what I want! btw, do you know how to complete it using `zod`? – yuusheng Aug 17 '23 at 08:10
  • `z.switch` seems like a correct api to use, given that zod is considering deprecation of `z.discriminatedUnion`. See: https://github.com/colinhacks/zod/issues/2106 – Lesiak Aug 17 '23 at 16:37