// Refer to https://github.com/type-challenges/type-challenges/issues/22316
// ===========
// == Utils ==
// ===========
// A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1170)
declare const UniqueSymbol: unique symbol
type UniqueSymbolType = typeof UniqueSymbol
type _FilterEmpty<B, D> =
Equal<B, null> extends true ? null :
Equal<B, undefined> extends true ? undefined :
D
type _PendingType = 1 // <T>() => T
type _StoreTagsByStringArray<TAGS extends string[]> = {
// The expression usage with `_{PendingType}` is meant to enable assignment among each tag.
// [UniqueSymbol]?: ([ TAGS ] | _PendingType) & _PendingType // It could solve issue① while comes another issue.
[UniqueSymbol]?: (TAGS | _PendingType) & _PendingType // Utilize issue① below
}
type _Includes<A extends readonly unknown[], B extends readonly unknown[]> =
A extends [...B, ...unknown[]]
? true
: A extends [unknown, ...infer Rest]
? _Includes<Rest, B>
: false
type _Union2Intersection<U> =
(U extends unknown ? (arg: U) => void : never) extends
(arg: infer I) => void ? I : never
type _GetTags<B> = [B] extends [never] ? [] : B extends _StoreTagsByStringArray<infer Tags extends string[]>
? Tags extends (infer S extends string[]) & _PendingType // Issue①: it would infer to `TAGS & _PendingType`.
? S
: []
: []
// ===========
// == Start ==
// ===========
type GetTags<B> = _Union2Intersection<(_GetTags<B>)> extends infer Result
? Result extends never
? []
: Result extends string[]
? Result
: never
: never
type Tag<B, T extends string, _UB = UnTag<B>> = _FilterEmpty<B, _UB & _StoreTagsByStringArray<[ ...GetTags<B>, T ]>>
type UnTag<B> = _FilterEmpty<B, Omit<B, UniqueSymbolType>>
type HasTag<B, T extends string> = _Includes<GetTags<B>, [T]>
type HasTags<B, T extends readonly string[]> = _Includes<GetTags<B>, T>
type HasExactTags<B, T extends readonly string[]> = Equal<GetTags<B>, T>
Solution by lvjiaxuan #22739
/**
* 版本一
* 清晰、精干,但是有未通过的测试用例(GetTags 处理 union 的部分)
* 这是我比较推崇的一版,比较适合学习
*/
// ---- Start: Structures ----
declare const UniqueSymbol: unique symbol
type UniqueSymbolType = typeof UniqueSymbol
type TagsWrapper<B, TGS> = UniqueSymbolType | (UniqueSymbolType & [B, TGS])
type TagsBag<B, TGS> = {
[UniqueSymbol]?: TagsWrapper<B, TGS>
}
// ---- End: Structures ----
// ---- Start: Get Tags ----
type GetTags<B> =
Equal<B, never> extends true ? [] :
B extends TagsBag<unknown, infer Tags extends string[]>
? Equal<Tags, string[]> extends true
? []
: Tags
: []
// ---- End: Get Tags ----
// ---- Start: Tag and UnTag ----
type PassEmptyValue<B, D> =
Equal<B, null> extends true ? null :
Equal<B, undefined> extends true ? undefined :
D
type Tag<B, TG> = PassEmptyValue<B, UnTag<B> & TagsBag<UnTag<B>, [...GetTags<B>, TG]>>
type UnTag<B> = PassEmptyValue<B, Omit<B, UniqueSymbolType>>
// ---- End: Tag and UnTag ----
// ---- Start: Other Methods ----
type Includes<A extends readonly unknown[], B extends readonly unknown[]> =
A extends [...B, ...unknown[]]
? true
: A extends [unknown, ...infer Rest]
? Includes<Rest, B>
: false
type HasTag<B, T extends string> = Includes<GetTags<B>, [T]>
type HasTags<B, T extends readonly string[]> = Includes<GetTags<B>, T>
type HasExactTags<B, T extends readonly string[]> = Equal<GetTags<B>, T>
// ---- End: Other Methods ----
/**
* 版本二
* 这一版是在版本一的基础上做了修改,将 GetTags 的返回值由交集变为并集,纯粹是为了满足测试用例的需要,增加了一些额外的逻辑
* 学完版本一之后,如果非得要通关,可以使用这一版
*/
// ---- Start: Structures ----
declare const UniqueSymbol: unique symbol
type UniqueSymbolType = typeof UniqueSymbol
type TagsWrapper<B, TGS> = UniqueSymbolType | (UniqueSymbolType & [B, TGS])
type TagsBag<B, TGS> = {
[UniqueSymbol]?: TagsWrapper<B, TGS>
}
// ---- End: Structures ----
// ---- Start: Get Tags ----
type Union2Intersection<U> =
(U extends unknown ? (arg: U) => void : never) extends
(arg: infer I) => void ? I : never
type _GetTags<B> =
Equal<B, never> extends true ? [] :
B extends TagsBag<unknown, infer Tags extends string[]>
? Equal<Tags, string[]> extends true
? []
: Tags
: []
type GetTags<B> =
Union2Intersection<(_GetTags<B>)> extends infer Result
? Equal<Result, never> extends true
? []
: Result extends string[]
? Result
: never
: never
// ---- End: Get Tags ----
// ---- Start: Wrap and Unwrap ----
type PassEmptyValue<B, D> =
Equal<B, null> extends true ? null :
Equal<B, undefined> extends true ? undefined :
D
type Tag<B, TG> = PassEmptyValue<B, UnTag<B> & TagsBag<UnTag<B>, [...GetTags<B>, TG]>>
type UnTag<B> = PassEmptyValue<B, Omit<B, UniqueSymbolType>>
// ---- End: Tag and UnTag ----
// ---- Start: Other Methods ----
type Includes<A extends readonly unknown[], B extends readonly unknown[]> =
A extends [...B, ...unknown[]]
? true
: A extends [unknown, ...infer Rest]
? Includes<Rest, B>
: false
type HasTag<B, T extends string> = Includes<GetTags<B>, [T]>
type HasTags<B, T extends readonly string[]> = Includes<GetTags<B>, T>
type HasExactTags<B, T extends readonly string[]> = Equal<GetTags<B>, T>
// ---- End: Other Methods ----
References
Solution by zhaoyao91 #22316
// ============= Helper Types =============
type IsAny<T> = 0 extends 1 & T ? true : false
type IsNever<T> = [T] extends [never] ? true : false
type RemoveIndexSignature<T> = {
[K in keyof T as Extract<string | number | symbol, K> extends never
? K
: never]: T[K]
}
type GetRootTagObject<B> = US extends keyof B
? Required<RemoveIndexSignature<Required<B>[US]>>
: never
// root object could be {}, but it's not a tag object, so treat root object specially
type GetTagObject<B> = B extends Record<any, unknown>
? Required<RemoveIndexSignature<B>>
: never
// multiple tag is nested
type TagObject<P, T extends string> = (IsNever<P> extends true
? {
[K in T]?: never
}
: {
[K in keyof P]?: TagObject<GetTagObject<P[K]>, T>
}) & {
// make object with difference tag compatible
[K in string]?: never
}
// ============= Function Types =============
type GetTags<
B,
O = IsAny<B> extends true ? never : GetRootTagObject<B>
> = IsNever<O> extends true
? []
: [keyof O, ...GetTags<B, GetTagObject<O[keyof O]>>]
type Tag<
B,
T extends string,
O = true extends IsAny<B> | IsNever<B> ? {} : B
> = [O] extends [null | undefined]
? O
: {
[K in Exclude<keyof O, US>]: O[K]
} & {
[K in US]?: TagObject<GetRootTagObject<O>, T>
}
type UnTag<B> = {
[K in keyof B as K extends US ? never : K]: B[K]
}
type HasTag<B, T extends string> = T extends GetTags<B>[number] ? true : false
type HasTags<
B,
T extends readonly string[],
U extends string[] = GetTags<B> extends string[] ? GetTags<B> : []
> = U extends [...T, ...unknown[]]
? true
: U extends [...unknown[], ...T]
? true
: false
type HasExactTags<
B,
T extends readonly string[],
U extends string[] = GetTags<B> extends string[] ? GetTags<B> : []
> = U extends T ? true : false
Solution by theoolee #19991
// your answers
type IsEqual<X,Y> =(<A>()=>A extends X ? 1 : 2) extends
(<B>()=>B extends Y ? 1 : 2)
? true
: false ;
declare const a:unique symbol;
type Unique =typeof a;
declare const d: unique symbol;
type CommonValue=typeof d;
declare const b:unique symbol;
type NonRegular = typeof b;
type GetTagValue<
Source> =
Unique extends keyof Source
? Exclude<keyof Exclude<Source[Unique],undefined>,symbol|number>
: never ;
type TagValueFromTagTuple<
T extends readonly string[],
TagValue extends string = never,
PossiblyEmptyValue extends string = [TagValue] extends [never] ? '' : TagValue> =
T extends readonly [infer R extends string,...infer U extends readonly string[]]
? U extends []
? TagValueFromTagTuple<U,`${PossiblyEmptyValue}${R}`>
: TagValueFromTagTuple<U,`${PossiblyEmptyValue}${R}|`>
: TagValue ;
type TagTupleFromTagValue<
TagValue extends string> =
[TagValue] extends [never]
? []
: TagValue extends `${infer R extends string}|${infer U extends string}`
? [R,...TagTupleFromTagValue<U>]
: [TagValue] ;
type GetTags<
Source> =
[Source] extends [never]
? []
: 1 extends 2&Source
? []
: TagTupleFromTagValue<GetTagValue<Source>> ;
type SetMarker<
Source,
NewTagValue extends string,
OldTagValue extends string =
GetTagValue<Source>,
TagValue extends string =
[OldTagValue] extends [never]
? `${NewTagValue}`
: `${OldTagValue}|${NewTagValue}` ,
Marker extends object = {[J in Unique]? : {[J in TagValue|CommonValue]? : true}} > =
NonRegular extends keyof Source
? {[J in NonRegular] : Source[J]}&Marker
: UnTag<Source>&Marker ;
type EffectiveSource<
Source,
> =
[IsEqual<Source,{}>&IsEqual<Source,never>&IsEqual<Source,any>] extends [never]
? {[J in NonRegular] : Source}
: Source ;
type Tag<
Source,
NewTagValue extends string,
> =
IsEqual<Source,undefined>&IsEqual<Source,null> extends [never]
? Source
: SetMarker<EffectiveSource<Source>,NewTagValue> ;
type UnTag<
Source,
Marker ={[J in Unique&keyof Source]? :Source[J]}> =
NonRegular extends keyof Source
? Source[NonRegular]
: Source extends infer R&Marker
? R
: Source ;
type UnionMemberHasTag<
Source,
TagValue extends string> =
Source extends Source
? [GetTagValue<Source>] extends [never]
? false
: GetTagValue<Source> extends `${infer R}${TagValue}${infer U}`
? true
: false
: never;
type UnionMemberHasExactTag<
Source,
TagValue extends string> =
Source extends Source
? IsEqual<GetTagValue<Source>,TagValue>
: never ;
type HasTag<
Source,
TagValue extends string> =
UnionMemberHasTag<Source,TagValue> extends true
? true
: false ;
type HasTags<
Source,
TagTuple extends readonly string[]> =
HasTag<Source,TagValueFromTagTuple<TagTuple>> ;
type HasExactTags<
Source,
TagTuple extends readonly string[]> =
TagTuple extends []
? true
: UnionMemberHasExactTag<Source,TagValueFromTagTuple<TagTuple>> extends true
? true
: false ;
Solution by justBadProgrammer #16642
interface Tagged {
[UNIQUE_SYMBOL]: () => string
}
type Is<A, B> = (<G>() => G extends A ? 1 : 0) extends (<G>() => G extends B ? 1 : 0) ? true : false
type Tagging<This, Tag extends string = string, NewTag extends Tagged = {[UNIQUE_SYMBOL]: (this: This) => Tag}> = true extends Is<This & NewTag, This> ? NewTag : ReturnType<<T extends This & NewTag>() => {[K in keyof T]: T[K]}>
type ExtractTagged<B>
= true extends Is<B, any> | Is<Extract<B, Tagged>, never> ? Tagging<B, ''>
: ExtractOne<Extract<B, Tagged>> extends infer S extends Tagged
? S extends Tagging<infer This>
? true extends Is<Exclude<B, S | This>, never> ? S : S | ExtractTagged<Exclude<B, S | This>>
: never /* infer X */
: never /* infer S */
type ExtractTags<B, I extends Tagged = ExtractTagged<B>, Tags = true extends Is<B, any> | Is<I, never> ? '' : I extends I ? ReturnType<I[US]> : never /* This extends I */>
= true extends Is<Tags, ExtractOne<Tags>> ? Tags : ''
type ExtractOne<U> = (U extends U ? (_: () => U) => void : never) extends (_: infer I extends () => U) => void ? ReturnType<I> : never
type Split<Tags extends string> = Tags extends '' ? [] : Tags extends `${infer Tag} ${infer Rest}` ? Tag extends '' ? Split<Rest> : [Tag, ...Split<Rest>] : [Tags]
type Join<Tags extends readonly string[]> = Tags extends [infer S extends string, ...infer R extends string[]] ? R extends [] ? S : `${S extends '' ? '' : `${S} `}${Join<R>}` : ''
type Append<Tags extends string, T extends string> = string extends Tags ? T : Tags extends '' ? T : ` ${Tags} ` extends ` ${T} ` ? Tags : `${Tags} ${T}`
type GetTags<B, Tags extends string = ExtractTags<B>> = true extends Is<Tags, never> ? [] : Split<Tags>
type Tag<B, T extends string, I extends Tagged = ExtractTagged<B>>
= true extends Is<I, never> ? B
: I extends Tagging<infer This, infer Tags extends string>
? Tagging<This, Append<Tags, T>> extends infer NewTagged
? true extends Is<This | NewTagged, This> ? NewTagged : This | NewTagged
: never /* infer NewI */
: never /* infer This, infer Tags */
type UnTag<B, I extends Tagged = ExtractTagged<B>>
= true extends Is<I, never> ? B
: I extends I ? I extends Tagging<infer This> ? This : never /* infer This */ : never /* I extends I */
type HasTag<B, T extends string, Tags extends string = ExtractTags<B>> = ` ${Tags} ` extends `${any} ${T} ${any}` ? true : false
type HasTags<B, T extends readonly string[], Tags extends string = ExtractTags<B>> = ` ${Tags} ` extends `${any} ${Join<T>} ${any}` ? true : false
type HasExactTags<B, T extends readonly string[], Tags extends string = ExtractTags<B>> = Tags extends Join<T> ? true : false
Solution by teamchong #11465
// your answers
declare const TAG: unique symbol
type TagHelper<B, T extends string> = typeof TAG extends keyof B
? Omit<B, typeof TAG> & { [TAG]: TagHelper<B[typeof TAG], T> }
: B & { [TAG]: { [K in T]: true } }
type Tag<B, T extends string> = B extends null | undefined ? B : TagHelper<B, T>
type GetTags<B> = typeof TAG extends keyof B
? [keyof Omit<B[typeof TAG], typeof TAG>, ...GetTags<B[typeof TAG]>]
: []
type Keys<A extends readonly any[], RR = never> = A extends [infer L, ...infer R]
? Keys<R, RR | L>
: RR
type HasTagsHelper<A, B extends readonly any[]> = B extends [infer L, ...infer R]
? L extends A
? HasTagsHelper<A, R>
: false
: true
type HasTags<B, T extends readonly string[]> = T extends []
? false
: HasTagsHelper<Keys<GetTags<B>>, T>
type HasTag<B, T extends string> = HasTags<B, [T]>
type HasExactTags<B, T extends readonly string[]> = GetTags<B> extends T ? true : false
type UnTag<B> = Omit<B, typeof TAG>
Solution by ZangYuSong #7175
declare const uq: unique symbol
declare const dummy: unique symbol
type UQ = typeof uq
type DUMMY = typeof dummy
type UU<T> = Exclude<T, undefined>
type GetTags<B>
= Equal<B, any> extends true ? []
: UQ extends keyof B ? [Exclude<keyof UU<B[UQ]>, UQ | DUMMY>, ...GetTags<UU<B[UQ]>>] : []
type TagRec<B, T extends string>
= UQ extends keyof B ? Omit<B, UQ> & {[uq]?: TagRec<UU<B[keyof B & UQ]>, T>}
: B & {[uq]?: {[P in T | DUMMY]?: 1}}
type Tag<B, T extends string>
= Equal<B, null> extends true ? B
: Equal<B, undefined> extends true ? B
: Equal<B, any> extends true ? TagRec<{dummy: 1}, T>
: Equal<B, never> extends true ? TagRec<{dummy: 2}, T>
: TagRec<B, T>
type Member<T extends any[], U> = U extends T[number] ? true : false
type PrefixUnion<T> = T extends [...infer L, infer R] ? T | PrefixUnion<L> : never
type Subseq<T, U>
= U extends PrefixUnion<T> ? true
: T extends [infer L, ...infer R] ? Subseq<R, U> : false
type UnTag<B> = UQ extends keyof B ? Omit<B, UQ> : B
type HasTag<B, T extends string> = (B extends any ? Member<GetTags<B>, T> : never) extends true ? true : false
type HasTags<B, T extends readonly string[]> = Subseq<GetTags<B>, T>
type HasExactTags<B, T extends readonly string[]> = GetTags<B> extends T ? true : false
Solution by okayu29 #6319
Current assertions will only pass if tags argument order matches tagging order
type Cases = [
// Should only pass if tags arguments is ["bar", "foo"]
Expect<Equal<HasTags<Tag<Tag<{}, "bar">, "foo">, ["foo", "bar"]>, false>>,
// Should only pass if tags arguments is ["foo", "baz"] or ["foo", "baz", 'bar]
Expect<Equal<HasTags<Tag<Tag<Tag<{}, "foo">, "baz">, "bar">, ["foo", "bar"]>, false>>,
]
Solution will only fail if a non existent tag is provided
type Cases = [
// Will pass for ["foo", "bar", "baz"], ["baz", "bar", "foo"], ...etc
Expect<Equal<HasTags<Tag<Tag<Tag<{}, "foo">, "baz">, "bar">, ["foo", "bar"]>, true>>,
]
type Flatten<Type> = { [Key in keyof Type]: Type[Key] }
type UnionToIntersection<Type> = (
Type extends any ? (wrapper: Type) => unknown :never
) extends (wrapper: infer WrappedType) => unknown ? WrappedType : never
type UnionToTuple<Type> = UnionToIntersection<
Type extends any ? (wrapper: Type) => unknown :never
> extends (wrapper: infer WrappedType) => unknown
? [...UnionToTuple<Exclude<Type, WrappedType>> ,WrappedType]
: []
/**
* Copy of:
* import { Equal } from "@type-challenges/utils";
*/
type Equivalent<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
declare const TagKey: unique symbol;
type Injectable<Target> = typeof TagKey extends keyof Target
? Omit<Target, never> // Preserve special types like any, unknown and never
: Target
type InjectTag<Target, Type = {}> = Injectable<Target> & { [TagKey]?: Type }
type Tag<
Target,
Type extends string
> = Equivalent<Target, null> extends true
? null
: Equivalent<Target, undefined> extends true
? undefined
: Injectable<Target> extends Injectable<InjectTag<infer RootTarget, infer RootType>>
? InjectTag<
RootTarget,
Flatten<
{ [Key in keyof RootType]?: RootType[Key]} &
{ [Key in Type]?: Type } &
Record<string, string>
>
>
: InjectTag<
Target,
Flatten<
{ [Key in Type]?: Type } &
Record<string, string>
>
>
type GetTagUnion<
Target
> = Target extends InjectTag<unknown, infer Tags>
? keyof { [Key in keyof Tags as string extends Key ? never : Key]: Tags[Key] }
: never
type GetCommonTagUnion<
Target,
TargetTuple = UnionToTuple<Target>
> = TargetTuple extends [infer CurrentTarget, ...infer RestTarget]
? RestTarget extends []
? GetTagUnion<CurrentTarget>
: GetTagUnion<CurrentTarget> & GetCommonTagUnion<RestTarget[number]>
: never
type GetTags<Target> = UnionToTuple<GetCommonTagUnion<Target>>
type UnTag<Target> = keyof Target extends never
? Target
: Omit<Target, typeof TagKey>
type HasTag<
Target,
Type extends string
> = Type extends GetCommonTagUnion<Target>
? true
: false
type HasTags<
Target,
Type extends ReadonlyArray<string>
> = Type[number] extends GetCommonTagUnion<Target>
? true
: false
type HasExactTags<
Target,
Type extends ReadonlyArray<string>
> = GetCommonTagUnion<Target> extends Type[number]
? true
: false
Solution by ianbunag #3768
type IsNever<T> = [T] extends [never] ? true : false
type IsAny<T> = 0 extends (1 & T) ? true : false
type Is<T, K> = [T] extends [K] ? [K] extends [T] ? true : false : false
type StartsWith<T extends any[], K extends any[]> =
T extends [infer T0, ...infer Tr]
? K extends [infer K0, ...infer Kr]
? Is<T0, K0> extends true
? StartsWith<Tr, Kr>
: false
: true
: [] extends K
? true
: false
type StartsWithTest = [
Expect<Equal<StartsWith<[2,0,2,1], [2,0]>, true>>,
Expect<Equal<StartsWith<[2,0,2,1], [2,1]>, false>>,
Expect<Equal<StartsWith<[2,0,2,1], [2]>, true>>,
Expect<Equal<StartsWith<[2,0,2,1], [2,0,2,1]>, true>>,
Expect<Equal<StartsWith<[2,0,2,1], [2,0,2,1,2]>, false>>,
Expect<Equal<StartsWith<[2,0,2,1], []>, true>>,
]
type SuccessionWith<T extends any[], K extends any[]> =
T extends [infer T0, ...infer Tr]
? K extends [infer K0, ...infer Kr]
? Is<T0, K0> extends true
? StartsWith<Tr, Kr>
: SuccessionWith<Tr, K>
: [] extends K
? true
: false
: false
type SuccessionWithTest = [
Expect<Equal<SuccessionWith<[2,0,2,1], [1,2]>, false>>,
Expect<Equal<SuccessionWith<[2,0,2,1], [2,0]>, true>>,
Expect<Equal<SuccessionWith<[2,0,2,1], [2]>, true>>,
Expect<Equal<SuccessionWith<[2,0,2,1], [2,0,2,1]>, true>>,
Expect<Equal<SuccessionWith<[2,0,2,1], [1,2,0,2,1]>, false>>,
Expect<Equal<SuccessionWith<[2,0,2,1], []>, true>>,
]
/**
* Define tag container as function
*/
type Tags<T extends string[], B> = () => [T, B]
/**
* Inject tags to any types except `null` and `undefined`.
*
* `null | (null & Tags<T, null>)` wold be evaluated to `null`, so null cannot be labeled with tag(s), and `undefined` is the same.
*/
type SetTags<B, T extends string[]> =
IsNever<B> extends true
? Tags<T, B>
: IsAny<B> extends true
? Tags<T, B>
: Is<B, unknown> extends true
? Tags<T, B>
: B | (B & Tags<T, B>)
/**
* Extract original type which already labeled with tags.
*
* If `B` is an Union Type, only labeled with tag type would be returned as Union Type.
*/
type GetTagged<B> = B extends Tags<infer _, infer T> ? T : never
/**
* Get tag string tuple from type `B`.
*
* If `B` is an Union Type which all members are labeled with tags, this type returns Union Type of tag tuples.
* When all members are labeled with exactly same tag(s), only one tuple will be retuned by Union Type's characteristics.
*/
type GetTagTupples<B> =
IsNever<Exclude<B, GetTagged<B>>> extends true
? IsNever<B> extends true
? []
: IsAny<B> extends true
? []
: B extends Tags<infer T, infer _>
? T
: never
: B extends Tags<infer T, never>
? T
: []
type Tag<B, T extends string> = SetTags<UnTag<B>, [...GetTags<B>, T]>
type UnTag<B> = B extends infer X ? X extends Tags<infer _, infer K> ? K : X : never
/**
* Get tag string tuple from type `B`.
*
* If `B` is an Union Type which all members are labeled with tags, this type returns Union Type of tag tuples.
* When all members are labeled with exactly same tag(s), only one tuple will be retuned by Union Type's characteristics.
*/
type GetTags<B> = GetTagTupples<Exclude<B, null | undefined>>
type HasTag<B, T extends string> = HasTags<B, [T]>
type HasTags<B, T extends readonly string[]> = SuccessionWith<GetTags<B>, [...T]> extends true ? true : false
type HasExactTags<B, T extends readonly string[]> = Is<GetTags<B>, T>
Solution by akipom #1947
This is a code-intensive task (although technically not very difficult), but it can be useful for organizing some kind of design-by-contract programming via types.
declare const KEY: unique symbol;
type Tuple = readonly string[];
interface Tagged {
readonly [KEY]?: unknown;
}
/**
* Shift<["foo", "bar", "baz"]> = ["bar", "baz"].
*/
type Shift<T extends Tuple> = T extends [infer Head, ...infer Tail] ? Tail : [];
/**
* StartWith<[], ["foo"]> = true.
* StartWith<["foo"], ["foo", "bar"]> = true.
* StartWith<["foo", "baz"], ["foo", "bar"]> = false.
* StartWith<["foo", "bar"], ["foo", "bar", "qux"]> = true.
*/
type StartWith<S extends Tuple, T extends Tuple> = S extends []
? true
: Equal<S[0], T[0]> extends false
? false
: StartWith<Shift<S>, Shift<T>>;
/**
* Includes<["foo", "bar"], ["quux", "foo", "bar", "qux"]> = true.
* Includes<["foo"], ["bar", "qux"]> = false.
*/
type Includes<S extends Tuple, T extends Tuple> = S extends []
? true
: T extends []
? false
: Equal<S[0], T[0]> extends true
? StartWith<S, T>
: Includes<S, Shift<T>>;
/**
* GetStringProps<{ 0: 0; x?: 3 }> = 3.
*/
type GetStringProps<T> = Exclude<
{
[K in keyof T & string]: T[K];
}[keyof T & string],
undefined
>;
/**
* GetStringKeys<{ 0: 0; x?: 3 }> = "x".
*/
type GetStringKeys<T> = Exclude<
{
[K in keyof T & string]: K;
}[keyof T & string],
undefined
>;
/**
* GetTagsKey<null> = "".
* GetTagsKey<Tag<string, "foo">> = "0foo".
* GetTagsKey<Tag<Tag<string, "foo">, "bar">> = "0foo1bar"
*/
type GetTagsKey<
V,
TagsOrUndefined = [V] extends [Tagged] ? V[typeof KEY] : undefined,
TagsKeyOrNever = GetStringKeys<Exclude<TagsOrUndefined, undefined>>
> = Equal<TagsKeyOrNever, never> extends true
? ""
: Equal<TagsKeyOrNever, string> extends true
? ""
: TagsKeyOrNever;
/**
* GetTags<null> = [].
* GetTags<any> = [].
* GetTags<Tag<string, "foo">> = ["foo"].
* GetTags<Tag<Tag<string, "foo">, "bar">> = ["foo", "bar"].
* GetTags<Tag<Tag<Tag<{}, "foo">, "bar">, "baz">> = ["foo", "bar", "baz"].
*/
export type GetTags<
V,
TagsOrUndefined = [V] extends [Tagged] ? V[typeof KEY] : undefined,
TagsOrNever = GetStringProps<Exclude<TagsOrUndefined, undefined>>
> = Equal<V, any> extends true
? []
: Equal<TagsOrNever, never> extends true
? []
: TagsOrNever extends Tuple
? TagsOrNever
: [];
/**
* Tag<number, "foo"> = number with tag "foo".
* Tag<{ x: 0 }, "foo"> = { x: 0 } with tag "foo".
* Tag<Tag<V, "foo">, "bar"> = V with tags "foo" and "bar".
*/
export type Tag<
V,
T extends string,
Tags extends Tuple = GetTags<V>,
TagsKey extends string = GetTagsKey<V>
> = Equal<V, null> extends true
? null
: Equal<V, undefined> extends true
? undefined
: (typeof KEY extends keyof V ? Omit<V, typeof KEY> : V) & {
readonly [KEY]?: { 0: 0 } & {
[K in `${TagsKey}${Tags["length"]}${T}`]?: [...Tags, T];
};
};
/**
* UnTag<null> = null.
* UnTag<undefined> = undefined.
* UnTag<Tag<{}, "foo">> = {}.
* UnTag<Tag<Tag<{ x: 0 }, "foo">, "bar">> = { x: 0 }.
*/
export type UnTag<V> = typeof KEY extends keyof V ? Omit<V, typeof KEY> : V;
/**
* HasTag<null, "foo"> = false.
* HasTag<Tag<{}, "foo">, "foo"> = true.
* HasTag<Tag<any, "foo">, "foo"> = true.
* HasTag<Tag<Tag<{}, "foo">, "bar">, "foo"> = true.
* HasTag<Tag<Tag<symbol, "bar">, "foo">, "foo"> = true.
* HasTag<Tag<Tag<{}, "bar">, "baz">, "foo"> = false.
*/
export type HasTag<V, T extends string> = Includes<[T], GetTags<V>>;
/**
* HasTags<null, ["foo"]> = false.
* HasTags<Tag<{}, "bar">, ["foo"]> = false.
* HasTags<Tag<any, "bar">, ["foo"]> = false.
* HasTags<Tag<{}, "foo">, ["foo"]> = true.
* HasTags<Tag<any, "foo">, ["foo"]> = true.
* HasTags<Tag<Tag<string, "foo">, "bar">, ["foo", "bar"]> = true.
* HasTags<Tag<Tag<{}, "bar">, "foo">, ["foo", "bar"]> = false.
* HasTags<Tag<Tag<Tag<{}, "baz">, "foo">, "bar">, ["foo", "bar"]> = true.
* HasTags<Tag<Tag<Tag<{}, "foo">, "bar">, "baz">, ["foo", "bar"]> = true.
* HasTags<Tag<Tag<Tag<{}, "foo">, "baz">, "bar">, ["foo", "bar"]> = false.
*/
export type HasTags<V, T extends Tuple> = Includes<T, GetTags<V>>;
/**
* HasExactTags<0, []> = true.
* HasExactTags<Tag<number, "foo">, ["foo"]> = true.
* HasExactTags<Tag<{}, "foo">, ["bar"]> = false.
* HasExactTags<Tag<Tag<any, "foo">, "bar">, ["foo", "bar"]> = true.
* HasExactTags<Tag<Tag<Tag<{}, "foo">, "bar">, "baz">, ["foo", "bar"]> = false.
* HasExactTags<Tag<Tag<Tag<{}, "foo">, "bar">, "baz">, ["foo", "bar", "baz"]> = true.
*/
export type HasExactTags<V, T extends Tuple> = Equal<GetTags<V>, T>;
Solution by uid11 #713