Skip to content

Add NonEmptyString type#1103

Merged
sindresorhus merged 10 commits intosindresorhus:mainfrom
jor1ken:feat/non-empty-string
Apr 15, 2025
Merged

Add NonEmptyString type#1103
sindresorhus merged 10 commits intosindresorhus:mainfrom
jor1ken:feat/non-empty-string

Conversation

@jor1ken
Copy link
Copy Markdown
Contributor

@jor1ken jor1ken commented Apr 8, 2025

closed #946

Comment on lines +7 to +11
declare const a: NonEmptyString<'a'>;
expectType<NonEmptyString<'a'>>(a);

declare const b: NonEmptyString<'b' | 'c'>;
expectType<NonEmptyString<'b' | 'c'>>(b);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected type and the actual type are identical, making these tests useless.

Suggested change
declare const a: NonEmptyString<'a'>;
expectType<NonEmptyString<'a'>>(a);
declare const b: NonEmptyString<'b' | 'c'>;
expectType<NonEmptyString<'b' | 'c'>>(b);
declare const a: NonEmptyString<'a'>;
expectType<'a'>(a);
declare const b: NonEmptyString<'b' | 'c'>;
expectType<'b' | 'c'>(b);

Comment on lines +7 to +11
declare const a: NonEmptyString<'a'>;
expectType<NonEmptyString<'a'>>(a);

declare const b: NonEmptyString<'b' | 'c'>;
expectType<NonEmptyString<'b' | 'c'>>(b);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, add these test cases:

expectType<'a'>({} as NonEmptyString<'' | 'a'>);
expectType<string>({} as NonEmptyString<string>);
expectType<Uppercase<string>>({} as NonEmptyString<Uppercase<string>>);
expectType<`on${string}`>({} as NonEmptyString<`on${string}`>);

Comment on lines +4 to +5
declare const empty: NonEmptyString<''>;
expectNever(empty);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use expectType instead of expectNever.

Suggested change
declare const empty: NonEmptyString<''>;
expectNever(empty);
declare const empty: NonEmptyString<''>;
expectType<never>(empty);

@som-sm som-sm changed the title Add NonEmptyString Add NonEmptyString type Apr 8, 2025
@sindresorhus
Copy link
Copy Markdown
Owner

I would like to see some more tests. Think of more edge-cases to test.


Here are some AI-generated suggestions:

// Tests that string types from mapped types work correctly
type StringMap = { [K in 'a' | 'b']: string };
type StringMapValues = StringMap[keyof StringMap];
expectType<string>({} as NonEmptyString<StringMapValues>);

// Tests handling of conditional types that resolve to empty string or string
type MaybeEmpty<T> = T extends number ? '' : string;
expectType<never>({} as NonEmptyString<MaybeEmpty<number>>);
expectType<string>({} as NonEmptyString<MaybeEmpty<boolean>>);

// Tests distribution over unions in conditional types
type UnionResult<T> = T extends string ? T | '' : never;
expectType<string>({} as NonEmptyString<UnionResult<string>>);

// Tests with generic constraints - this shows behavior with type parameters
declare function genericTest<T extends string, U extends ''>(
    a: NonEmptyString<T>,
    b: NonEmptyString<U>
): void;

@som-sm
Copy link
Copy Markdown
Collaborator

som-sm commented Apr 9, 2025

Here are some AI-generated suggestions:

@sindresorhus These AI generated tests aren't actually doing anything different—they just look complicated. Given that the tests suggested here are added.

Let's break them down:

// Tests that string types from mapped types work correctly
type StringMap = { [K in 'a' | 'b']: string };
type StringMapValues = StringMap[keyof StringMap];
expectType<string>({} as NonEmptyString<StringMapValues>);

This is effectively just NonEmptyString<string>, since StringMapValues is simply string. Probably the actual intention here was to test NonEmptyString<"a" | "b">, but even that wouldn't be something different.


// Tests handling of conditional types that resolve to empty string or string
type MaybeEmpty<T> = T extends number ? '' : string;
expectType<never>({} as NonEmptyString<MaybeEmpty<number>>);
expectType<string>({} as NonEmptyString<MaybeEmpty<boolean>>);

This just tests NonEmptyString<''> and NonEmptyString<string>. Nothing new here either!


// Tests distribution over unions in conditional types
type UnionResult<T> = T extends string ? T | '' : never;
expectType<string>({} as NonEmptyString<UnionResult<string>>);

Again, this simplifies down to NonEmptyString<string>. Most likely, the intention here was to test something like NonEmptyString<'' | 'a'>, but again that wouldn't be anything new.


// Tests with generic constraints - this shows behavior with type parameters
declare function genericTest<T extends string, U extends ''>(
	a: NonEmptyString<T>,
	b: NonEmptyString<U>
): void;

And this one doesn't seem to test anything at all.


The only meaningful edge case I can think of is testing for generic assignability.

// `NonEmptyString<S>` should be assignable to `string`
type Assignability1<_S extends string> = unknown;
type Test1<S extends string> = Assignability1<NonEmptyString<S>>;

// `string` should NOT be assignable to `NonEmptyString<S>`
type Assignability2<_S extends string, _SS extends NonEmptyString<_S>> = unknown;
// @ts-expect-error
type Test2<S extends string> = Assignability2<S, S>;

@som-sm
Copy link
Copy Markdown
Collaborator

som-sm commented Apr 9, 2025

On second thought, I believe we should return never for non-literal string types that include the empty string—such as string, Uppercase<string>, etc. Here's why:

// If `string_` is typed as `NonEmptyString<T>`, the implementation of `foo` likely assumes
// that `string_` should never be an empty string at runtime.
declare function foo<T extends string>(string_: NonEmptyString<T>): void;
declare const someString: string;

foo(someString); // No error, because `NonEmptyString<string>` is just `string`, 
// but `someString` could be an empty string at runtime.

And this behavior is also inconsistent with the following:

declare const aOrBOrEmpty: "a" | "b" | "";

foo(aOrBOrEmpty); // Errors
// Argument of type '"" | "a" | "b"' is not assignable to parameter of type '"a" | "b"'.
// Type '""' is not assignable to type '"a" | "b"'.

If "a" | "b" | "" causes an error due to the presence of "", then string should as well—since it also includes the empty string.

@sindresorhus @jor1ken WDYT?

@sindresorhus
Copy link
Copy Markdown
Owner

@sindresorhus @jor1ken WDYT?

👍

@som-sm som-sm force-pushed the feat/non-empty-string branch from 72b1ed2 to 78ef575 Compare April 9, 2025 09:03
Copy link
Copy Markdown
Collaborator

@som-sm som-sm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sindresorhus I've updated the implementation to handle this and also updated the tests, please have a look!

@sindresorhus sindresorhus merged commit 19a9c37 into sindresorhus:main Apr 15, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-Empty String Type

3 participants