Skip to content

Commit

Permalink
feat(cdk): add TuiLooseUnion (#9068)
Browse files Browse the repository at this point in the history
  • Loading branch information
splincode authored Sep 18, 2024
1 parent 36ee705 commit e7642cb
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 23 deletions.
1 change: 1 addition & 0 deletions projects/cdk/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './context';
export * from './handler';
export * from './loose-union';
export * from './mapper';
export * from './matcher';
export * from './rounding';
Expand Down
46 changes: 46 additions & 0 deletions projects/cdk/types/loose-union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @example
* type Color = 'primary' | 'secondary' | string;
*
* Your brand has some known colors, like `primary` and `secondary`.
* But you also want to make sure that users can specify any color they want.
*
* @Input() color: Color = '';
*
* <my-icon color="red" />
*
* But there's an issue. We aren't getting `color`
* suggestions when we use the `MyIcon` component.
* If we try to autocomplete the `color` prop, we get no suggestions.
* Ideally, we want `primary` and `secondary` to be part of that list.
* How do we manage that?
*
* This works because of a quirk of the TypeScript compiler.
* When you create a union between a string literal type and `string`,
* TypeScript will eagerly widen the type to `string`.
*
* But if we change type to
* type Color = 'primary' | 'secondary' | (string & {});
*
* TypeScript has already forgotten that `"primary"` and `"secondary"`
* were ever part of the type. But by intersecting `string` with
* an empty object, we trick TypeScript into retaining the string
* literal types for a bit longer.
*
* This might feel pretty fragile to you. This doesn't seem like
* intended behavior from TypeScript. Well, the team actually
* know about this trick. They test against it. And someday, they may make
* it so that a plain `string` type will just work.
* But until then, keep this in mind.
*/
export type TuiLooseUnion<U> =
| U
| (U extends string
? Record<never, never> & string
: U extends number
? Record<never, never> & number
: U extends symbol
? Record<never, never> & symbol
: U extends bigint
? Record<never, never> & bigint
: never);
5 changes: 3 additions & 2 deletions projects/core/directives/appearance/appearance.options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {ExistingProvider, ProviderToken} from '@angular/core';
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';
import {tuiCreateToken, tuiProvide} from '@taiga-ui/cdk/utils/miscellaneous';

/**
* Bundled appearances for autocomplete purposes, not exported on purpose
*/
type Appearance =
type Appearance = TuiLooseUnion<
| 'accent'
| 'destructive'
| 'error'
Expand All @@ -22,7 +23,7 @@ type Appearance =
| 'textfield'
| 'warning'
| 'whiteblock'
| (Record<never, never> & string);
>;

export interface TuiAppearanceOptions {
readonly appearance: Appearance;
Expand Down
19 changes: 7 additions & 12 deletions projects/core/pipes/flag/flag.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import type {PipeTransform} from '@angular/core';
import {inject, Pipe} from '@angular/core';
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';
import {TUI_ASSETS_PATH} from '@taiga-ui/core/tokens';
import type {TuiCountryIsoCode} from '@taiga-ui/i18n/types';

type IsoCode = TuiLooseUnion<TuiCountryIsoCode>;

@Pipe({
standalone: true,
name: 'tuiFlag',
})
export class TuiFlagPipe implements PipeTransform {
private readonly staticPath = inject(TUI_ASSETS_PATH);

public transform(
countryIsoCode: TuiCountryIsoCode | (Record<never, never> & string),
): string;
public transform(
countryIsoCode: TuiCountryIsoCode | (Record<never, never> & string) | undefined,
): string | null;
public transform(
countryIsoCode?: TuiCountryIsoCode | (Record<never, never> & string) | null,
): string | null;
public transform(
countryIsoCode?: TuiCountryIsoCode | (Record<never, never> & string) | null,
): string | null {
public transform(countryIsoCode: IsoCode): string;
public transform(countryIsoCode: IsoCode | undefined): string | null;
public transform(countryIsoCode?: IsoCode | null): string | null;
public transform(countryIsoCode?: IsoCode | null): string | null {
if (!countryIsoCode) {
return null;
}
Expand Down
6 changes: 4 additions & 2 deletions projects/i18n/types/language-names.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type TuiLanguageName =
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';

export type TuiLanguageName = TuiLooseUnion<
| 'belarusian'
| 'chinese'
| 'dutch'
Expand All @@ -18,4 +20,4 @@ export type TuiLanguageName =
| 'turkish'
| 'ukrainian'
| 'vietnamese'
| (Record<never, never> & string);
>;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Directive, Input} from '@angular/core';
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';

@Directive({
standalone: true,
selector: '[tuiSlot]',
})
export class TuiBadgedContentDirective {
@Input()
public tuiSlot: 'bottom' | 'top' | (Record<never, never> & string) = 'top';
public tuiSlot: TuiLooseUnion<'bottom' | 'top'> = 'top';
}
3 changes: 2 additions & 1 deletion projects/layout/components/app-bar/app-bar.directive.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Directive, Input} from '@angular/core';
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';

@Directive({
standalone: true,
selector: '[tuiSlot]',
})
export class TuiAppBarDirective {
@Input()
public tuiSlot: 'left' | 'right' | (Record<never, never> & string) = 'left';
public tuiSlot: TuiLooseUnion<'left' | 'right'> = 'left';
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Directive, Input} from '@angular/core';
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';

@Directive({
standalone: true,
selector: '[tuiSlot]',
})
export class TuiBlockStatusDirective {
@Input()
public tuiSlot: 'action' | 'top' | (Record<never, never> & string) = 'top';
public tuiSlot: TuiLooseUnion<'action' | 'top'> = 'top';
}
6 changes: 2 additions & 4 deletions projects/legacy/directives/wrapper/wrapper.directive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Directive, Input} from '@angular/core';
import type {TuiLooseUnion} from '@taiga-ui/cdk/types';
import type {TuiInteractiveState} from '@taiga-ui/core/types';

/**
Expand Down Expand Up @@ -46,10 +47,7 @@ export class TuiWrapperDirective {
return this.focus && !this.disabled;
}

protected get interactiveState():
| TuiInteractiveState
| (Record<never, never> & string)
| null {
protected get interactiveState(): TuiLooseUnion<TuiInteractiveState> | null {
if (this.disabled) {
return 'disabled';
}
Expand Down

0 comments on commit e7642cb

Please sign in to comment.